[
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n*.sh text eol=lf\nsupervisor.conf text eol=lf\nsystemd.service text eol=lf\n# Using the HEREDOC feature expects LF:\nDockerfile      text eol=lf\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: technitium # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\n[Xx]64/\n[Xx]86/\n[Bb]uild/\nbld/\n[Bb]in/\n[Oo]bj/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# DNX\nproject.lock.json\nartifacts/\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n\n# TODO: Un-comment the next line if you do not want to checkin \n# your web deploy settings because they may include unencrypted\n# passwords\n#*.pubxml\n*.publishproj\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n# NuGet v3's project.json files produces more ignoreable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Microsoft Azure ApplicationInsights config file\nApplicationInsights.config\n\n# Windows Store app package directory\nAppPackages/\nBundleArtifacts/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n[Ss]tyle[Cc]op.*\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.pfx\n*.publishsettings\nnode_modules/\norleans.codegen.cs\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# LightSwitch generated files\nGeneratedArtifacts/\nModelManifest.xml\n\n# Paket dependency manager\n.paket/paket.exe\n\n# FAKE - F# Make\n.fake/\n\nOther/\n"
  },
  {
    "path": "APIDOCS.md",
    "content": "# Technitium DNS Server API Documentation\n\nTechnitium 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.\n\nThe 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.\n\n## API Request\n\nUnless 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`.\n\nNote! 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.\n\n## API Response Format\n\nThe HTTP API returns a JSON formatted response for all requests. The JSON object returned contains `status` property which indicate if the request was successful. \n\nThe `status` property can have following values:\n- `ok`: This indicates that the call was successful.\n- `error`: This response tells the call failed and provides additional properties that provide details about the error.\n- `invalid-token`: When a session has expired or an invalid token was provided this response is received.\n- `2fa-required`: When a user has two-factor authentication enabled and the OTP was not provided during login, change password, etc. API calls.\n\nA 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.\n\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\nIn 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.\n\n```\n{\n\t\"status\": \"error\",\n\t\"errorMessage\": \"error message\",\n\t\"stackTrace\": \"application stack trace\",\n\t\"innerErrorMessage\": \"inner exception message\"\n}\n```\n\n## Name Server Address Format\n\nThe 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.\n\n- 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.\n- 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.\n- 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.\n- 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`\n- 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.\n- 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`\n\n## User API Calls\n\nThese 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.\n\n### Login\n\nThis 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.\n\nURL:\\\n`http://localhost:5380/api/user/login?user=admin&pass=admin&includeInfo=true`\n\nOBSOLETE PATH:\\\n`/api/login`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `user`: The username for the user account. The built-in administrator username on the DNS server is `admin`.\n- `pass`: The password for the user account. The default password for `admin` user is `admin`. \n- `totp` (optional): The time-based one-time password for the user account if it has Two Factor Authentication (2FA) enabled.\n- `includeInfo` (optional): Includes basic info relevant for the user in response.\n\nWARNING: It is highly recommended to change the password on first use to avoid security related issues.\n\nRESPONSE:\n```\n{\n\t\"displayName\": \"Administrator\",\n\t\"username\": \"admin\",\n\t\"totpEnabled\": false,\n\t\"token\": \"932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9\",\n\t\"info\": {\n\t\t\"version\": \"14.3\",\n\t\t\"dnsServerDomain\": \"server1\",\n\t\t\"defaultRecordTtl\": 3600,\n\t\t\"defaultNsRecordTtl\": 14400,\n\t\t\"defaultSoaRecordTtl\": 900,\n\t\t\"permissions\": {\n\t\t\t\"Dashboard\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Zones\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Cache\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Allowed\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Blocked\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Apps\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"DnsClient\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Settings\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"DhcpServer\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Administration\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Logs\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t}\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\nWHERE:\n- `token`: Is the session token generated that MUST be used with all subsequent API calls.\n\n### Create API Token\n\nAllows 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.\n\nURL:\\\n`http://localhost:5380/api/user/createToken?user=admin&pass=admin&tokenName=MyToken1`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `user`: The username for the user account for which to generate the API token.\n- `pass`: The password for the user account.\n- `totp` (optional): The time-based one-time password for the user account if it has Two Factor Authentication (2FA) enabled.\n- `tokenName`: The name of the created token to identify its session.\n\nRESPONSE:\n```\n{\n\t\"username\": \"admin\",\n\t\"tokenName\": \"MyToken1\",\n\t\"token\": \"932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9\",\n\t\"status\": \"ok\"\n}\n```\n\nWHERE:\n- `token`: Is the session token generated that MUST be used with all subsequent API calls.\n\n### Logout\n\nThis call ends the session generated by the `login` or the `createToken` call. The `token` would no longer be valid after calling the `logout` API.\n\nURL:\\\n`http://localhost:5380/api/user/logout?token=932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9`\n\nOBSOLETE PATH:\\\n`/api/logout`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Get Session Info\n\nReturns the same info as that of the `login` or the `createToken` calls for the session specified by the token.\n\nURL:\\\n`http://localhost:5380/api/user/session/get?token=932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"displayName\": \"Administrator\",\n\t\"username\": \"admin\",\n\t\"totpEnabled\": false,\n\t\"token\": \"932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9\",\n\t\"info\": {\n\t\t\"version\": \"14.0\",\n\t\t\"uptimestamp\": \"2023-07-29T08:01:31.1117463Z\",\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"clusterInitialized\": true,\n\t\t\"clusterDomain\": \"example.com\"\n\t\t\"defaultRecordTtl\": 3600,\n\t\t\"useSoaSerialDateScheme\": false,\n\t\t\"dnssecValidation\": true,\n\t\t\"permissions\": {\n\t\t\t\"Dashboard\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Zones\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Cache\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Allowed\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Blocked\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Apps\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"DnsClient\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Settings\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"DhcpServer\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Administration\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t\"Logs\": {\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t}\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete User Session\n\nAllows deleting a session for the current user.\n\nURL:\\\n`http://localhost:5380/api/user/session/delete?token=x&partialToken=620c3bfcd09d0a07`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `partialToken`: The partial token as returned by the user profile details API call.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Change Password\n\nAllows changing the password for the current logged in user account.\n\nNOTE: It is highly recommended to change the `admin` user password on first use to avoid security related issues.\n\nURL:\\\n`http://localhost:5380/api/user/changePassword?token=x&pass=password&newPass=newpassword`\n\nOBSOLETE PATH:\\\n`/api/changePassword`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated only by the `login` call.\n- `pass`: The current password for the currently logged in user.\n- `newPass`: The new password to be set for the currently logged in user.\n- `totp` (optional): The 6-digit code from the authenticator app if the user has 2FA enabled.\n- `iterations` (optional): The number of iterations for PBKDF2 SHA256 password hashing.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Initialize 2FA\n\nInitializes 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.\n\nURL:\\\n`http://localhost:5380/api/user/2fa/init?token=x`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated only by the `login` call.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"totpEnabled\": false,\n\t\t\"qrCodePngImage\": \"iVBORw0KGgoAAAANSUhEU...\",\n\t\t\"secret\": \"RZ56CYOXKAXI5D23\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Enable 2FA\n\nEnables two-factor authentication for the current logged in user account. This API call can be called only after the Initialize 2FA API call.\n\nURL:\\\n`http://localhost:5380/api/user/2fa/enable?token=x`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated only by the `login` call.\n- `totp`: The 6-digit code from the authenticator app.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Disable 2FA\n\nDisables two-factor authentication for the current logged in user account.\n\nURL:\\\n`http://localhost:5380/api/user/2fa/disable?token=x`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated only by the `login` call.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Get User Profile Details\n\nGets the user account profile details.\n\nURL:\\\n`http://localhost:5380/api/user/profile/get?token=x`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"displayName\": \"Administrator\",\n\t\t\"username\": \"admin\",\n\t\t\"totpEnabled\": false,\n\t\t\"disabled\": false,\n\t\t\"previousSessionLoggedOn\": \"2022-09-15T12:59:05.944Z\",\n\t\t\"previousSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"recentSessionLoggedOn\": \"2022-09-15T13:57:50.1843973Z\",\n\t\t\"recentSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"sessionTimeoutSeconds\": 1800,\n\t\t\"memberOfGroups\": [\n\t\t\t\"Administrators\"\n\t\t],\n\t\t\"sessions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": true,\n\t\t\t\t\"partialToken\": \"620c3bfcd09d0a07\",\n\t\t\t\t\"type\": \"Standard\",\n\t\t\t\t\"tokenName\": null,\n\t\t\t\t\"lastSeen\": \"2022-09-15T13:58:02.4728Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set User Profile Details\n\nAllows changing user account profile values.\n\nURL:\\\n`http://localhost:5380/api/user/profile/set?token=x&displayName=Administrator&sessionTimeoutSeconds=1800`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated only by the `login` call.\n- `displayName` (optional): The display name to set for the user account.\n- `sessionTimeoutSeconds` (optional): The session timeout value to set in seconds for the user account.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"displayName\": \"Administrator\",\n\t\t\"username\": \"admin\",\n\t\t\"totpEnabled\": false,\n\t\t\"disabled\": false,\n\t\t\"previousSessionLoggedOn\": \"2022-09-15T12:59:05.944Z\",\n\t\t\"previousSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"recentSessionLoggedOn\": \"2022-09-15T13:57:50.1843973Z\",\n\t\t\"recentSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"sessionTimeoutSeconds\": 1800,\n\t\t\"memberOfGroups\": [\n\t\t\t\"Administrators\"\n\t\t],\n\t\t\"sessions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": true,\n\t\t\t\t\"partialToken\": \"620c3bfcd09d0a07\",\n\t\t\t\t\"type\": \"Standard\",\n\t\t\t\t\"tokenName\": null,\n\t\t\t\t\"lastSeen\": \"2022-09-15T14:00:50.288738Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Check For Update\n\nThis call requests the server to check for software update.\n\nURL:\\\n`http://localhost:5380/api/user/checkForUpdate?token=x`\n\nOBSOLETE PATH:\\\n`/api/checkForUpdate`\n\nPERMISSIONS:\\\nNone\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"updateAvailable\": true,\n\t\t\"updateVersion\": \"9.0\",\n\t\t\"currentVersion\": \"8.1.4\",\n\t\t\"updateTitle\": \"New Update Available!\",\n\t\t\"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.\",\n\t\t\"downloadLink\": \"https://download.technitium.com/dns/DnsServerSetup.zip\",\n\t\t\"instructionsLink\": \"https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html\",\n\t\t\"changeLogLink\": \"https://github.com/TechnitiumSoftware/DnsServer/blob/master/CHANGELOG.md\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n## Dashboard API Calls\n\nThese API calls provide access to dashboard stats and allow deleting stat files.\n\n### Get Stats\n\nReturns the DNS stats that are displayed on the web console dashboard.\n\nURL:\\\n`http://localhost:5380/api/dashboard/stats/get?token=x&type=LastHour&utc=true`\n\nOBSOLETE PATH:\\\n`api/getStats`\n\nPERMISSIONS:\\\nDashboard: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `type` (optional): The duration type for which valid values are: [`LastHour`, `LastDay`, `LastWeek`, `LastMonth`, `LastYear`, `Custom`]. Default value is `LastHour`.\n- `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`.\n- `dontTrimQueryTypeData` (optional): Set to `true` to get full data for query type chart instead of top 10 entries. Default value is `false` when unspecified.\n- `start` (optional): The start date in ISO 8601 format. Applies only to `custom` type.\n- `end` (optional): The end date in ISO 8601 format. Applies only to `custom` type.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"stats\": {\n\t\t\t\"totalQueries\": 925,\n\t\t\t\"totalNoError\": 834,\n\t\t\t\"totalServerFailure\": 1,\n\t\t\t\"totalNxDomain\": 90,\n\t\t\t\"totalRefused\": 0,\n\t\t\t\"totalAuthoritative\": 47,\n\t\t\t\"totalRecursive\": 348,\n\t\t\t\"totalCached\": 481,\n\t\t\t\"totalBlocked\": 49,\n\t\t\t\"totalDropped\": 0,\n\t\t\t\"totalClients\": 6,\n\t\t\t\"zones\": 19,\n\t\t\t\"cachedEntries\": 6330,\n\t\t\t\"allowedZones\": 10,\n\t\t\t\"blockedZones\": 1,\n\t\t\t\"allowListZones\": 0,\n\t\t\t\"blockListZones\": 307447\n\t\t},\n\t\t\"mainChartData\": {\n\t\t\t\"labelFormat\": \"HH:mm\",\n\t\t\t\"labels\": [\n\t\t\t\t\"2024-02-04T10:38:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:39:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:40:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:41:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:42:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:43:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:44:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:45:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:46:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:47:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:48:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:49:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:50:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:51:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:52:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:53:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:54:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:55:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:56:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:57:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:58:00.0000000Z\",\n\t\t\t\t\"2024-02-04T10:59:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:00:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:01:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:02:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:03:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:04:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:05:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:06:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:07:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:08:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:09:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:10:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:11:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:12:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:13:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:14:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:15:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:16:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:17:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:18:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:19:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:20:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:21:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:22:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:23:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:24:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:25:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:26:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:27:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:28:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:29:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:30:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:31:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:32:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:33:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:34:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:35:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:36:00.0000000Z\",\n\t\t\t\t\"2024-02-04T11:37:00.0000000Z\"\n\t\t\t],\n\t\t\t\"datasets\": [\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Total\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(102, 153, 255, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(102, 153, 255)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t13,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t27,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t15,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t17,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t61,\n\t\t\t\t\t\t23,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t21,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t20,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t35,\n\t\t\t\t\t\t26,\n\t\t\t\t\t\t33,\n\t\t\t\t\t\t20,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t14,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t19,\n\t\t\t\t\t\t37,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t18,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t30,\n\t\t\t\t\t\t47,\n\t\t\t\t\t\t16,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t37,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t18,\n\t\t\t\t\t\t22,\n\t\t\t\t\t\t16,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t15,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t41,\n\t\t\t\t\t\t13,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t17,\n\t\t\t\t\t\t12\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"No Error\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(92, 184, 92, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(92, 184, 92)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t22,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t13,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t13,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t59,\n\t\t\t\t\t\t22,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t21,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t19,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t31,\n\t\t\t\t\t\t25,\n\t\t\t\t\t\t24,\n\t\t\t\t\t\t16,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t19,\n\t\t\t\t\t\t36,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t18,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t28,\n\t\t\t\t\t\t46,\n\t\t\t\t\t\t16,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t34,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t13,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t15,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t40,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t14,\n\t\t\t\t\t\t12\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Server Failure\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(217, 83, 79, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(217, 83, 79)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"NX Domain\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(120, 120, 120, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(120, 120, 120)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t9,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t0\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Refused\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(91, 192, 222, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(91, 192, 222)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Authoritative\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(150, 150, 0, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(150, 150, 0)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Recursive\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(23, 162, 184, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(23, 162, 184)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t14,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t36,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t14,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t26,\n\t\t\t\t\t\t17,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t18,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t20,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t21,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t5\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Cached\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(111, 84, 153, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(111, 84, 153)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t11,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t23,\n\t\t\t\t\t\t14,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t13,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t13,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t18,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t15,\n\t\t\t\t\t\t12,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t18,\n\t\t\t\t\t\t41,\n\t\t\t\t\t\t15,\n\t\t\t\t\t\t7,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t14,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t19,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t8,\n\t\t\t\t\t\t7\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Blocked\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(255, 165, 0, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(255, 165, 0)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t6,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t5,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t0\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Dropped\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(30, 30, 30, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(30, 30, 30)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"label\": \"Clients\",\n\t\t\t\t\t\"backgroundColor\": \"rgba(51, 122, 183, 0.1)\",\n\t\t\t\t\t\"borderColor\": \"rgb(51, 122, 183)\",\n\t\t\t\t\t\"borderWidth\": 2,\n\t\t\t\t\t\"fill\": true,\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t3,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t3\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"queryResponseChartData\": {\n\t\t\t\"labels\": [\n\t\t\t\t\"Authoritative\",\n\t\t\t\t\"Recursive\",\n\t\t\t\t\"Cached\",\n\t\t\t\t\"Blocked\",\n\t\t\t\t\"Dropped\"\n\t\t\t],\n\t\t\t\"datasets\": [\n\t\t\t\t{\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t47,\n\t\t\t\t\t\t348,\n\t\t\t\t\t\t481,\n\t\t\t\t\t\t49,\n\t\t\t\t\t\t0\n\t\t\t\t\t],\n\t\t\t\t\t\"backgroundColor\": [\n\t\t\t\t\t\t\"rgba(150, 150, 0, 0.5)\",\n\t\t\t\t\t\t\"rgba(23, 162, 184, 0.5)\",\n\t\t\t\t\t\t\"rgba(111, 84, 153, 0.5)\",\n\t\t\t\t\t\t\"rgba(255, 165, 0, 0.5)\",\n\t\t\t\t\t\t\"rgba(7, 7, 7, 0.5)\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"queryTypeChartData\": {\n\t\t\t\"labels\": [\n\t\t\t\t\"A\",\n\t\t\t\t\"HTTPS\",\n\t\t\t\t\"AAAA\",\n\t\t\t\t\"SOA\",\n\t\t\t\t\"SRV\"\n\t\t\t],\n\t\t\t\"datasets\": [\n\t\t\t\t{\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t683,\n\t\t\t\t\t\t196,\n\t\t\t\t\t\t42,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t2\n\t\t\t\t\t],\n\t\t\t\t\t\"backgroundColor\": [\n\t\t\t\t\t\t\"rgba(102, 153, 255, 0.5)\",\n\t\t\t\t\t\t\"rgba(92, 184, 92, 0.5)\",\n\t\t\t\t\t\t\"rgba(7, 7, 7, 0.5)\",\n\t\t\t\t\t\t\"rgba(91, 192, 222, 0.5)\",\n\t\t\t\t\t\t\"rgba(150, 150, 0, 0.5)\",\n\t\t\t\t\t\t\"rgba(23, 162, 184, 0.5)\",\n\t\t\t\t\t\t\"rgba(111, 84, 153, 0.5)\",\n\t\t\t\t\t\t\"rgba(255, 165, 0, 0.5)\",\n\t\t\t\t\t\t\"rgba(51, 122, 183, 0.5)\",\n\t\t\t\t\t\t\"rgba(150, 150, 150, 0.5)\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"protocolTypeChartData\": {\n\t\t\t\"labels\": [\n\t\t\t\t\"Udp\"\n\t\t\t],\n\t\t\t\"datasets\": [\n\t\t\t\t{\n\t\t\t\t\t\"data\": [\n\t\t\t\t\t\t925\n\t\t\t\t\t],\n\t\t\t\t\t\"backgroundColor\": [\n\t\t\t\t\t\t\"rgba(111, 84, 153, 0.5)\",\n\t\t\t\t\t\t\"rgba(150, 150, 0, 0.5)\",\n\t\t\t\t\t\t\"rgba(23, 162, 184, 0.5)\",\n\t\t\t\t\t\t\"rgba(255, 165, 0, 0.5)\",\n\t\t\t\t\t\t\"rgba(91, 192, 222, 0.5)\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"topClients\": [\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.5\",\n\t\t\t\t\"domain\": \"server1.home\",\n\t\t\t\t\"hits\": 463,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.12\",\n\t\t\t\t\"domain\": \"vostro1.home\",\n\t\t\t\t\"hits\": 236,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.13\",\n\t\t\t\t\"hits\": 165,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.11\",\n\t\t\t\t\"domain\": \"shreyas-zare.home\",\n\t\t\t\t\"hits\": 53,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.15\",\n\t\t\t\t\"domain\": \"android-9c3d70b130d99b94.home\",\n\t\t\t\t\"hits\": 6,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.2\",\n\t\t\t\t\"domain\": \"pi1.home\",\n\t\t\t\t\"hits\": 2,\n\t\t\t\t\"rateLimited\": false\n\t\t\t}\n\t\t],\n\t\t\"topDomains\": [\n\t\t\t{\n\t\t\t\t\"name\": \"hses7-vod-cf-ace.cdn.hotstar.com\",\n\t\t\t\t\"hits\": 114\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"bifrost-api.hotstar.com\",\n\t\t\t\t\"hits\": 61\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"edge.microsoft.com\",\n\t\t\t\t\"hits\": 52\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"www.google.com\",\n\t\t\t\t\"hits\": 34\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"www.hotstar.com\",\n\t\t\t\t\"hits\": 24\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"safebrowsing.googleapis.com\",\n\t\t\t\t\"hits\": 15\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"www.bing.com\",\n\t\t\t\t\"hits\": 14\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"go.microsoft.com\",\n\t\t\t\t\"hits\": 14\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"graph.facebook.com\",\n\t\t\t\t\"hits\": 13\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"substrate.office.com\",\n\t\t\t\t\"hits\": 11\n\t\t\t}\n\t\t],\n\t\t\"topBlockedDomains\": [\n\t\t\t{\n\t\t\t\t\"name\": \"mobile.pipe.aria.microsoft.com\",\n\t\t\t\t\"hits\": 10\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"in.api.glance.inmobi.com\",\n\t\t\t\t\"hits\": 9\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"in.analytics.glance.inmobi.com\",\n\t\t\t\t\"hits\": 6\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"app-measurement.com\",\n\t\t\t\t\"hits\": 4\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"googleads.g.doubleclick.net\",\n\t\t\t\t\"hits\": 4\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"analytics.swiggy.com\",\n\t\t\t\t\"hits\": 2\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"firebase-settings.crashlytics.com\",\n\t\t\t\t\"hits\": 2\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"cdn.cookielaw.org\",\n\t\t\t\t\"hits\": 2\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"m.urbancompany.com\",\n\t\t\t\t\"hits\": 1\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"beacons.gvt2.com\",\n\t\t\t\t\"hits\": 1\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get Top Stats\n\nReturns the top stats data for specified stats type.\n\nURL:\\\n`http://localhost:5380/api/dashboard/stats/getTop?token=x&type=LastHour&statsType=TopClients&limit=1000`\n\nOBSOLETE PATH:\\\n`/api/getTopStats`\n\nPERMISSIONS:\\\nDashboard: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `type` (optional): The duration type for which valid values are: [`LastHour`, `LastDay`, `LastWeek`, `LastMonth`, `LastYear`, `custom`]. Default value is `LastHour`.\n- `start` (optional): The start date in ISO 8601 format. Applies only to `custom` type.\n- `end` (optional): The end date in ISO 8601 format. Applies only to `custom` type.\n- `statsType`: The stats type for which valid values are : [`TopClients`, `TopDomains`, `TopBlockedDomains`]\n- `limit` (optional): The limit of records to return. Default value is `1000`.\n- `noReverseLookup` (optional): Set to `true` to disable reverse lookup for Top Clients list. This option is only applicable with `TopClients` stats type.\n- `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.\n\nRESPONSE:\nThe 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`:\n```\n{\n\t\"response\": {\n\t\t\"topClients\": [\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.5\",\n\t\t\t\t\"domain\": \"server1.local\",\n\t\t\t\t\"hits\": 236,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.4\",\n\t\t\t\t\"domain\": \"nas1.local\",\n\t\t\t\t\"hits\": 16,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.6\",\n\t\t\t\t\"domain\": \"server2.local\",\n\t\t\t\t\"hits\": 14,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"192.168.10.3\",\n\t\t\t\t\"domain\": \"nas2.local\",\n\t\t\t\t\"hits\": 12,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"217.31.193.175\",\n\t\t\t\t\"domain\": \"condor175.knot-resolver.cz\",\n\t\t\t\t\"hits\": 10,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"162.158.180.45\",\n\t\t\t\t\"hits\": 9,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"217.31.193.163\",\n\t\t\t\t\"domain\": \"gondor-resolver.labs.nic.cz\",\n\t\t\t\t\"hits\": 9,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"210.245.24.68\",\n\t\t\t\t\"hits\": 8,\n\t\t\t\t\"rateLimited\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"101.91.16.140\",\n\t\t\t\t\"hits\": 8,\n\t\t\t\t\"rateLimited\": false\n\t\t\t}\n\t\t],\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete All Stats\n\nPermanently 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.\n\nURL:\\\n`http://localhost:5380/api/dashboard/stats/deleteAll?token=x`\n\nOBSOLETE PATH:\\\n`/api/deleteAllStats`\n\nPERMISSIONS:\\\nDashboard: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n## Authoritative Zone API Calls\n\nThese API calls allow managing all hosted zones on the DNS server.\n\n### List Zones\n\nList 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.\n\nURL:\\\n`http://localhost:5380/api/zones/list?token=x&pageNumber=1&zonesPerPage=10`\n\nOBSOLETE PATH:\\\n`/api/zone/list`\\\n`/api/listZones`\n\nPERMISSIONS:\\\nZones: View\\\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"pageNumber\": 1,\n\t\t\"totalPages\": 2,\n\t\t\"totalZones\": 12,\n\t\t\"zones\": [\n\t\t\t{\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"dnssecStatus\": \"SignedWithNSEC\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"expiry\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"isExpired\": false,\n\t\t\t\t\"syncFailed\": false,\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"0.in-addr.arpa\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": true,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"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\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": true,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"127.in-addr.arpa\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": true,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"255.in-addr.arpa\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": true,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": false,\n\t\t\t\t\"dnssecStatus\": \"SignedWithNSEC\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"notifyFailed\": false,\n\t\t\t\t\"notifyFailedFor\": [],\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"localhost\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": true,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"test0.com\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": false,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"notifyFailed\": false,\n\t\t\t\t\"notifyFailedFor\": [],\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"test1.com\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": false,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"notifyFailed\": false,\n\t\t\t\t\"notifyFailedFor\": [],\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"test2.com\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"internal\": false,\n\t\t\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\t\t\"soaSerial\": 1,\n\t\t\t\t\"notifyFailed\": false,\n\t\t\t\t\"notifyFailedFor\": [],\n\t\t\t\t\"lastModified\": \"2022-02-26T07:57:08.1842183Z\",\n\t\t\t\t\"disabled\": false\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### List Catalog Zones\n\nReturns a list of Catalog zone names.\n\nURL:\\\n`http://localhost:5380/api/zones/catalogs/list?token=x`\n\nPERMISSIONS:\\\nZones: View\\\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"catalogZoneNames\": [\n\t\t\t\"catalog1\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Create Zone\n\nCreates a new authoritative zone.\n\nURL:\\\n`http://localhost:5380/api/zones/create?token=x&zone=example.com&type=Primary`\n\nOBSOLETE PATH:\\\n`/api/zone/create`\\\n`/api/createZone`\n\nPERMISSIONS:\\\nZones: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `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.\n- `type`: The type of zone to be created. Valid values are [`Primary`, `Secondary`, `Stub`, `Forwarder`, `SecondaryForwarder`, `Catalog`, `SecondaryCatalog`].\n- `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.\n- `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`.\n- `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.\n- `zoneTransferProtocol` (optional): The zone transfer protocol to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones. Valid values are [`Tcp`, `Tls`, `Quic`].\n- `tsigKeyName` (optional): The TSIG key name to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones.\n- `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.\n- `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`.\n- `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.\n- `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.\n- `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.\n- `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.\n- `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.\n- `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.\n- `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.\n- `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.\n\nREQUEST: 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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"domain\": \"example.com\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\nWHERE:\n- `domain`: Will contain the zone that was created. This is specifically useful to know the reverse zone that was created.\n\n### Import Zone \n\nAllows importing a complete zone file or a set of DNS resource records in standard RFC 1035 zone file format.\n\nURL:\\\n`http://localhost:5380/api/zones/import?token=x&zone=example.com&overwrite=true&overwriteSoaSerial=false`\n\nPERMISSIONS:\\\nZones: Modify\nZone: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to import.\n- `overwrite` (optional): Set to `true` to allow overwriting existing resource record set for the records being imported.\n- `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.\n\nREQUEST: 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.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Export Zone\n\nExports the complete zone in standard RFC 1035 zone file format.\n\nURL:\\\n`http://localhost:5380/api/zones/export?token=x&zone=example.com`\n\nPERMISSIONS:\\\nZones: View\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to export.\n\nRESPONSE: Response is a downloadable text file with `Content-Type: text/plain` and `Content-Disposition: attachment`.\n\n### Clone Zone\n\nClones an existing zone with all the records to create a new zone.\n\nURL:\\\n`http://localhost:5380/api/zones/clone?token=x&zone=example.com&sourceZone=template.com`\n\nPERMISSIONS:\\\nZones: Modify\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to be created.\n- `sourceZone`: The domain name of the zone to be cloned.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Convert Zone Type\n\nConverts zone from one type to another.\n\nURL:\\\n`http://localhost:5380/api/zones/convert?token=x&zone=example.com&type=Primary`\n\nPERMISSIONS:\\\nZones: Delete\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to be converted.\n- `type`: The zone type to convert the current zone to.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Enable Zone\n\nEnables an authoritative zone.\n\nURL:\\\n`http://localhost:5380/api/zones/enable?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/enable`\\\n`/api/enableZone`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to be enabled.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Disable Zone\n\nDisables an authoritative zone. This will prevent the DNS server from responding for queries to this zone.\n\nURL:\\\n`http://localhost:5380/api/zones/disable?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/disable`\\\n`/api/disableZone`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to be disabled.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Zone\n\nDeletes an authoritative zone.\n\nURL:\\\n`http://localhost:5380/api/zones/delete?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/delete`\\\n`/api/deleteZone`\n\nPERMISSIONS:\\\nZones: Delete\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to be deleted.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Resync Zone\n\nAllows resyncing a Secondary or Stub zone. This process will re-fetch all the records from the primary name server for the zone.\n\nURL:\\\n`http://localhost:5380/api/zones/resync?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/resync`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to resync.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Get Zone Options\n\nGets the zone specific options.\n\nURL:\\\n`http://localhost:5380/api/zones/options/get?token=x&zone=example.com&includeAvailableTsigKeyNames=true`\n\nOBSOLETE PATH:\\\n`/api/zone/options`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to get options.\n- `includeAvailableCatalogZoneNames`: Set to `true` to include list of available Catalog zone names on the DNS server.\n- `includeAvailableTsigKeyNames`: Set to `true` to include list of available TSIG key names on the DNS server.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"name\": \"example.com\",\n\t\t\"type\": \"Primary\",\n\t\t\"internal\": false,\n\t\t\"dnssecStatus\": \"Unsigned\",\n\t\t\"notifyFailed\": true,\n\t\t\"notifyFailedFor\": [\n\t\t\t\"192.168.10.5\"\n\t\t],\n\t\t\"disabled\": false,\n\t\t\"catalog\": \"catalog1\",\n\t\t\"overrideCatalogQueryAccess\": false,\n\t\t\"overrideCatalogZoneTransfer\": false,\n\t\t\"overrideCatalogNotify\": false,\n\t\t\"queryAccess\": \"Allow\",\n\t\t\"queryAccessNetworkACL\": [],\n\t\t\"zoneTransfer\": \"AllowOnlyZoneNameServers\",\n\t\t\"zoneTransferNetworkACL\": [],\n\t\t\"zoneTransferTsigKeyNames\": [\n\t\t\t\"key.example.com\"\n\t\t],\n\t\t\"notify\": \"ZoneNameServers\",\n\t\t\"notifyNameServers\": [],\n\t\t\"update\": \"UseSpecifiedNetworkACL\",\n\t\t\"updateNetworkACL\": [\n\t\t\t\"192.168.180.0/24\"\n\t\t],\n\t\t\"updateSecurityPolicies\": [\n\t\t\t{\n\t\t\t\t\"tsigKeyName\": \"key.example.com\",\n\t\t\t\t\"domain\": \"example.com\",\n\t\t\t\t\"allowedTypes\": [\n\t\t\t\t\t\"A\",\n\t\t\t\t\t\"AAAA\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"tsigKeyName\": \"key.example.com\",\n\t\t\t\t\"domain\": \"*.example.com\",\n\t\t\t\t\"allowedTypes\": [\n\t\t\t\t\t\"ANY\"\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\t\"availableCatalogZoneNames\": [\n\t\t\t\"catalog1\"\n\t\t],\n\t\t\"availableTsigKeyNames\": [\n\t\t\t\"key.example.com\",\n\t\t\t\"catalog\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set Zone Options\n\nSets the zone specific options.\n\nURL:\\\n`http://localhost:5380/api/zones/options/set?token=x&zone=example.com&disabled=false&zoneTransfer=Allow&zoneTransferNameServers=&notify=ZoneNameServers&notifyNameServers=`\n\nOBSOLETE PATH:\\\n`/api/zone/options`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to set options.\n- `disabled` (optional): Sets if the zone is enabled or disabled.\n- `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.\n- `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.\n- `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.\n- `overrideCatalogNotify` (optional): Set to `true` to override Notify option in the Catalog zone.  This option is valid only for `Primary`, and `Forwarder` zones.\n- `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.\n- `primaryZoneTransferProtocol `(optional): The zone transfer protocol to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones. Valid values are [`Tcp`, `Tls`, `Quic`].\n- `primaryZoneTransferTsigKeyName` (optional): The TSIG key name to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones for zone transfer.\n- `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.\n- `queryAccess` (optional): Valid options are [`Deny`, `Allow`, `AllowOnlyPrivateNetworks`, `AllowOnlyZoneNameServers`, `UseSpecifiedNetworkACL`, `AllowZoneNameServersAndUseSpecifiedNetworkACL`].\n- `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`.\n- `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.\n- `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`.\n- `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.\n- `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.\n- `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.\n- `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.\n- `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`].\n- `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`.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Get Zone Permissions\n\nGets the zone specific permissions.\n\nURL:\\\n`http://localhost:5380/api/zones/permissions/get?token=x&zone=example.com&includeUsersAndGroups=true`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to get the permissions for.\n- `includeUsersAndGroups`: Set to true to get a list of users and groups in the response.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"section\": \"Zones\",\n\t\t\"subItem\": \"example.com\",\n\t\t\"userPermissions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t}\n\t\t],\n\t\t\"groupPermissions\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t}\n\t\t],\n\t\t\"users\": [\n\t\t\t\"admin\",\n\t\t\t\"shreyas\"\n\t\t],\n\t\t\"groups\": [\n\t\t\t\"Administrators\",\n\t\t\t\"DHCP Administrators\",\n\t\t\t\"DNS Administrators\",\n\t\t\t\"Everyone\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set Zone Permissions\n\nSets the zone specific permissions.\n\nURL:\\\n`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`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The domain name of the zone to get the permissions for.\n- `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\n- `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\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"section\": \"Zones\",\n\t\t\"subItem\": \"example.com\",\n\t\t\"userPermissions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t}\n\t\t],\n\t\t\"groupPermissions\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Sign Zone\n\nSigns the primary zone (DNSSEC).\n\nURL:\\\n`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`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/sign`\n\nPERMISSONS:\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone to sign.\n- `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`].\n- `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.\n- `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.\n- `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.\n- `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.\n- `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.\n- `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.\n- `dnsKeyTtl` (optional): The TTL value to be used for DNSKEY records. Default value is `86400` when not specified.\n- `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.\n- `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.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Unsign Zone\n\nUnsigns the primary zone (DNSSEC).\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/unsign?token=x&zone=example.com\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/unsign`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone to unsign.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Get DS Info\n\nGet the DS info for the signed primary zone to help with updating DS records at the parent zone.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/viewDS?token=x&zone=example.com\n\nPERMISSIONS:\\\nZones: View\\\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the signed primary zone.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"name\": \"example.com\",\n\t\t\"type\": \"Primary\",\n\t\t\"internal\": false,\n\t\t\"disabled\": false,\n\t\t\"dnssecStatus\": \"SignedWithNSEC\",\n\t\t\"dsRecords\": [\n\t\t\t{\n\t\t\t\t\"keyTag\": 47972,\n\t\t\t\t\"dnsKeyState\": \"Published\",\n\t\t\t\t\"dnsKeyStateReadyBy\": \"2023-10-29T16:20:08.8007369Z\",\n\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\"publicKey\": \"TK5a8pXPMspDwuh4Z3evOfNZm9kkc8IzwZDiCgIX6imxwkbpY9FTvhoI/ttZiLWZ5hvLbvrpsbd0liqSwqNmPg==\",\n\t\t\t\t\"digests\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"digestType\": \"SHA256\",\n\t\t\t\t\t\t\"digest\": \"D59EBB413C88576B519B2980DF50493689A4A260383D0CB2F260251D5CA2E144\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"digestType\": \"SHA384\",\n\t\t\t\t\t\t\"digest\": \"F8235EEAB1AEBCFAD28096DF8DCF820F25C685041562AAB63E1A3E1AC89D2FC3836E97114A64EC0E057DCA234451E50C\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get DNSSEC Properties\n\nGet the DNSSEC properties for the primary zone.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/get?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/getProperties`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"name\": \"example.com\",\n\t\t\"type\": \"Primary\",\n\t\t\"internal\": false,\n\t\t\"disabled\": false,\n\t\t\"dnssecStatus\": \"SignedWithNSEC\",\n\t\t\"dnsKeyTtl\": 3600,\n\t\t\"dnssecPrivateKeys\": [\n\t\t\t{\n\t\t\t\t\"keyTag\": 15048,\n\t\t\t\t\"keyType\": \"KeySigningKey\",\n\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\"state\": \"Published\",\n\t\t\t\t\"stateChangedOn\": \"2022-12-18T14:39:50.0328321Z\",\n\t\t\t\t\"stateReadyBy\": \"2022-12-18T16:14:50.0328321Z\",\n\t\t\t\t\"isRetiring\": false,\n\t\t\t\t\"rolloverDays\": 0\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"keyTag\": 46152,\n\t\t\t\t\"keyType\": \"ZoneSigningKey\",\n\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\"state\": \"Active\",\n\t\t\t\t\"stateChangedOn\": \"2022-12-18T14:39:50.0661173Z\",\n\t\t\t\t\"isRetiring\": false,\n\t\t\t\t\"rolloverDays\": 90\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Convert To NSEC\n\nConverts a primary zone from NSEC3 to NSEC for proof of non-existence.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/convertToNSEC?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/convertToNSEC`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Convert To NSEC3\n\nConverts a primary zone from NSEC to NSEC3 for proof of non-existence.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/convertToNSEC3?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/convertToNSEC3`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Update NSEC3 Parameters\n\nUpdates the iteration and salt length parameters for NSEC3.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/updateNSEC3Params?token=x&zone=example.com&iterations=0&saltLength=0`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/updateNSEC3Params`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n- `iterations` (optional): The number of iterations to use for hashing. Default value is `0` when not specified.\n- `saltLength` (optional): The length of salt in bytes to use for hashing. Default value is `0` when not specified.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Update DNSKEY TTL\n\nUpdates the TTL value for DNSKEY resource record set. The value can be updated only when all the DNSKEYs are in ready or active state.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/updateDnsKeyTtl?token=x&zone=example.com&ttl=86400`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/updateDnsKeyTtl`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n- `ttl`: The TTL value for the DNSKEY resource record set.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Add Private Key\n\nAdds a private key to be used for signing the zone with DNSSEC.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/addPrivateKey?token=x&zone=example.com&keyType=KeySigningKey&algorithm=ECDSA&curve=P256`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/generatePrivateKey`\\\n`/api/zones/dnssec/properties/generatePrivateKey`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n- `keyType`: The type of key for which the private key is to be generated. Valid values are [`KeySigningKey`, `ZoneSigningKey`].\n- `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).\n- `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`].\n- `pemPrivateKey` (optional): Specifies a user generated private key in PEM format to add. When not specified a private key will be automatically generated.\n- `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.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Update Private Key\n\nUpdates the DNSSEC private key properties.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/updatePrivateKey?token=x&zone=example.com&keyTag=1234&rolloverDays=90`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/updatePrivateKey`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n- `keyTag`: The key tag of the private key to be updated.\n- `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. \n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Private Key\n\nDeletes a private key that has state set as `Generated`. Private keys with any other state cannot be delete.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/deletePrivateKey?token=x&zone=example.com&keyTag=12345`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/deletePrivateKey`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n- `keyTag`: The key tag of the private key to be deleted.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Publish All Private Keys\n\nPublishes 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`.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/publishAllPrivateKeys?token=x&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/publishAllPrivateKeys`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Rollover DNSKEY\n\nGenerates 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.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/rolloverDnsKey?token=x&zone=example.com&keyTag=12345`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/rolloverDnsKey`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n- `keyTag`: The key tag of the private key to rollover.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Retire DNSKEY\n\nRetires 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.\n\nURL:\\\n`http://localhost:5380/api/zones/dnssec/properties/retireDnsKey?token=x&zone=example.com&keyTag=12345`\n\nOBSOLETE PATH:\\\n`/api/zone/dnssec/retireDnsKey`\n\nPERMISSIONS:\\\nZones: Modify\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `zone`: The name of the primary zone.\n- `keyTag`: The key tag of the private key to retire.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Add Record\n\nAdds an resource record for an authoritative zone.\n\nURL:\\\n`http://localhost:5380/api/zones/records/add?token=x&domain=example.com&zone=example.com`\n\nOBSOLETE PATH:\\\n`/api/zone/addRecord`\\\n`/api/addRecord`\n\nPERMISSIONS:\\\nZones: None\\\nZone: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `domain`: The domain name of the zone to add record.\n- `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used.\n- `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.\n- `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.\n- `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.\n- `comments` (optional): Sets comments for the added resource record.\n- `expiryTtl` (optional): Set to automatically delete the record when the value in seconds elapses since the record’s last modified time.\n- `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.\n- `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.\n- `createPtrZone` (optional): Set this option to `true` to create a reverse zone for PTR record. This option is used for `A` and `AAAA` records.\n- `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.\n- `nameServer` (optional): The name server domain name. This option is required for adding `NS` record.\n- `glue` (optional): This is the glue address for the name server in the `NS` record. This optional parameter is used for adding `NS` record.\n- `cname` (optional): The CNAME domain name. This option is required for adding `CNAME` record.\n- `ptrName` (optional): The PTR domain name. This option is required for adding `PTR` record.\n- `exchange` (optional): The exchange domain name. This option is required for adding `MX` record.\n- `preference` (optional): This is the preference value for `MX` record type. This option is required for adding `MX` record.\n- `text` (optional): The text data for `TXT` record. This option is required for adding `TXT` record.\n- `splitText` (optional): Set to `true` for using new line char to split text into multiple character-strings for adding `TXT` record.\n- `mailbox` (optional): Set an email address for adding `RP` record.\n- `txtDomain` (optional): Set a `TXT` record's domain name for adding `RP` record.\n- `priority` (optional): This parameter is required for adding the `SRV` record.\n- `weight` (optional): This parameter is required for adding the `SRV` record.\n- `port` (optional): This parameter is required for adding the `SRV` record.\n- `target` (optional): This parameter is required for adding the `SRV` record.\n- `naptrOrder` (optional): This parameter is required for adding the `NAPTR` record.\n- `naptrPreference` (optional): This parameter is required for adding the `NAPTR` record.\n- `naptrFlags` (optional): This parameter is required for adding the `NAPTR` record.\n- `naptrServices` (optional): This parameter is required for adding the `NAPTR` record.\n- `naptrRegexp` (optional): This parameter is required for adding the `NAPTR` record.\n- `naptrReplacement` (optional): This parameter is required for adding the `NAPTR` record.\n- `dname` (optional): The DNAME domain name. This option is required for adding `DNAME` record.\n- `keyTag` (optional): This parameter is required for adding `DS` record.\n- `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.\n- `digestType` (optional): Valid values are [`SHA1`, `SHA256`, `GOST-R-34-11-94`, `SHA384`]. This parameter is required for adding `DS` record.\n- `digest` (optional): A hex string value. This parameter is required for adding `DS` record.\n- `sshfpAlgorithm` (optional): Valid values are [`RSA`, `DSA`, `ECDSA`, `Ed25519`, `Ed448`]. This parameter is required for adding `SSHFP` record.\n- `sshfpFingerprintType` (optional): Valid values are [`SHA1`, `SHA256`]. This parameter is required for adding `SSHFP` record.\n- `sshfpFingerprint` (optional): A hex string value. This parameter is required for adding `SSHFP` record.\n- `tlsaCertificateUsage` (optional): Valid values are [`PKIX-TA`, `PKIX-EE`, `DANE-TA`, `DANE-EE`]. This parameter is required for adding `TLSA` record.\n- `tlsaSelector` (optional): Valid values are [`Cert`, `SPKI`]. This parameter is required for adding `TLSA` record.\n- `tlsaMatchingType` (optional): Valid value are [`Full`, `SHA2-256`, `SHA2-512`]. This parameter is required for adding `TLSA` record.\n- `tlsaCertificateAssociationData` (optional): A X509 certificate in PEM format or a hex string value. This parameter is required for adding `TLSA` record.\n- `svcPriority` (optional): The priority value for `SVCB` or `HTTPS` record. This parameter is required for adding `SCVB` or `HTTPS` record.\n- `svcTargetName` (optional): The target domain name for `SVCB` or `HTTPS` record. This parameter is required for adding `SCVB` or `HTTPS` record.\n- `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.\n- `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.\n- `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.\n- `uriPriority` (optional): The priority value for adding the `URI` record.\n- `uriWeight` (optional): The weight value for adding the `URI` record.\n- `uri` (optional): The URI value for adding the `URI` record.\n- `flags` (optional): This parameter is required for adding the `CAA` record.\n- `tag` (optional): This parameter is required for adding the `CAA` record.\n- `value` (optional): This parameter is required for adding the `CAA` record.\n- `aname` (optional): The ANAME domain name. This option is required for adding `ANAME` record.\n- `protocol` (optional): This parameter is required for adding the `FWD` record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`].\n- `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.\n- `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.\n- `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`.\n- `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.\n- `proxyAddress` (optional): The proxy server address to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `proxyPort` (optional): The proxy server port to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `proxyUsername` (optional): The proxy server username to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `proxyPassword` (optional): The proxy server password to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `appName` (optional): The name of the DNS app. This parameter is required for adding the `APP` record.\n- `classPath` (optional): This parameter is required for adding the `APP` record.\n- `recordData` (optional): This parameter is used for adding the `APP` record as per the DNS app requirements.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"zone\": {\n\t\t\t\"name\": \"example.com\",\n\t\t\t\"type\": \"Primary\",\n\t\t\t\"internal\": false,\n\t\t\t\"dnssecStatus\": \"SignedWithNSEC\",\n\t\t\t\"disabled\": false\n\t\t},\n\t\t\"addedRecord\": {\n\t\t\t\"disabled\": false,\n\t\t\t\"name\": \"example.com\",\n\t\t\t\"type\": \"A\",\n\t\t\t\"ttl\": 3600,\n\t\t\t\"rData\": {\n\t\t\t\t\"ipAddress\": \"3.3.3.3\"\n\t\t\t},\n\t\t\t\"dnssecStatus\": \"Unknown\",\n\t\t\t\"lastUsedOn\": \"0001-01-01T00:00:00\"\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get Records\n\nGets all records for a given authoritative zone.\n\nURL:\\\n`http://localhost:5380/api/zones/records/get?token=x&domain=example.com&zone=example.com&listZone=true`\n\nOBSOLETE PATH:\\\n`/api/zone/getRecords`\\\n`/api/getRecords`\n\nPERMISSIONS:\\\nZones: None\\\nZone: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `domain`: The domain name of the zone to get records.\n- `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"zone\": {\n\t\t\t\"name\": \"example.com\",\n\t\t\t\"type\": \"Primary\",\n\t\t\t\"internal\": false,\n\t\t\t\"dnssecStatus\": \"SignedWithNSEC3\",\n\t\t\t\"disabled\": false\n\t\t},\n\t\t\"records\": [\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"A\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"ipAddress\": \"1.1.1.1\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"NS\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"nameServer\": \"server1\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"SOA\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"primaryNameServer\": \"server1\",\n\t\t\t\t\t\"responsiblePerson\": \"hostadmin.example.com\",\n\t\t\t\t\t\"serial\": 35,\n\t\t\t\t\t\"refresh\": 900,\n\t\t\t\t\t\"retry\": 300,\n\t\t\t\t\t\"expire\": 604800,\n\t\t\t\t\t\"minimum\": 900\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"NSEC3PARAM\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 2,\n\t\t\t\t\t\"originalTtl\": 900,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:45:31Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:45:31Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"vJ/fXkGKsapdvWjDhcfHsBxpZhSzMRLZv3/bEGJ4N3/K7jiM92Ik336W680SI7g+NyPCQ3gqE7ta/JEL4bht4Q==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"SOA\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 2,\n\t\t\t\t\t\"originalTtl\": 900,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T12:53:39Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T11:53:39Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"9PQHH3ZGCuFRYkn28SoilS8y8zszgeOpCfJpIOAaE5ao+iBPCXudHacr/EpgB2wLzXpRjR+WgiYjmJH17+6bKg==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"A\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 2,\n\t\t\t\t\t\"originalTtl\": 3600,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:25:35Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:25:35Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"dWjn5hTWuEq57ncwGdVq+kdbMuFtuxLuZhYCcQMdsTxYkM/64RrPY6eYwfYQ7+fY1+QBSX2WudAM4dzbmL/s2A==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"NS\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 2,\n\t\t\t\t\t\"originalTtl\": 3600,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:25:35Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:25:35Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"Yx+leBcYNFf0gUfN6rECWrUZwCDhJbAGk1BNOJN01nPakS5meSbDApUHJZeAzfSBcPzodK3ddmEuhho1MABaZw==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 86400,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"DNSKEY\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 2,\n\t\t\t\t\t\"originalTtl\": 86400,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T12:27:09Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T11:27:09Z\",\n\t\t\t\t\t\"keyTag\": 65078,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"KWAK7o+FjJ2/6ZvX4C1wB41yRzlmec5pR2TTeNWlY/weg0MNKCLRs3uTopSjoTih+uq3IRR7Zx0iOcy7evOitA==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 86400,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"DNSKEY\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 2,\n\t\t\t\t\t\"originalTtl\": 86400,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T12:27:09Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T11:27:09Z\",\n\t\t\t\t\t\"keyTag\": 52896,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"oHtt1gUmDXxI5GMfS+LJ6uxKUcuUu+5EELXdhLrbk5V/yganP6sMgA4hGkzokYM22LDowjSdO5qwzCW6IDgKxg==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"DNSKEY\",\n\t\t\t\t\"ttl\": 86400,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"flags\": \"SecureEntryPoint, ZoneKey\",\n\t\t\t\t\t\"protocol\": 3,\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"publicKey\": \"dMRyc/Pji31mF3iHNrybPzbgvtb2NKtmXhjQq433BHI= ZveDa1z00VxDnugV1x7EDvpt+42TDh8OQwp1kOrpX0E=\",\n\t\t\t\t\t\"computedKeyTag\": 65078,\n\t\t\t\t\t\"dnsKeyState\": \"Ready\",\n\t\t\t\t\t\"computedDigests\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"digestType\": \"SHA256\",\n\t\t\t\t\t\t\t\"digest\": \"BBE017B17E5CB5FFFF1EC2C7815367DF80D8E7EAEE4832D3ED192159D79B1EEB\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"digestType\": \"SHA384\",\n\t\t\t\t\t\t\t\"digest\": \"0B0C9F1019BD3FE62C8B71F8C80E7A833BA468A7E303ABC819C0CB9BEDE8E26BB50CB1729547BFCCE2AE22390E44CDA3\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"DNSKEY\",\n\t\t\t\t\"ttl\": 86400,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"flags\": \"ZoneKey\",\n\t\t\t\t\t\"protocol\": 3,\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"publicKey\": \"IUvzTkf4JPg+7k57cQw7n7SR6/1dH7FaKxu9Cf+kcvo= UU+uoKRWnYAFHDNF0X3U8ZYetUyDF7fcNAwEaSQnIUM=\",\n\t\t\t\t\t\"computedKeyTag\": 61009,\n\t\t\t\t\t\"dnsKeyState\": \"Active\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"DNSKEY\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"flags\": \"SecureEntryPoint, ZoneKey\",\n\t\t\t\t\t\"protocol\": 3,\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"publicKey\": \"KOJWFitKm58EgjO43GDnsFbnkGoqVKeLRkP8FGPAdhqA2F758Ta1mkxieEu0YN0EoX+u5bVuc5DEBFSv+U63CA==\",\n\t\t\t\t\t\"computedKeyTag\": 15048,\n\t\t\t\t\t\"dnsKeyState\": \"Published\",\n\t\t\t\t\t\"dnsKeyStateReadyBy\": \"2022-12-18T16:14:50.0328321Z\",\n\t\t\t\t\t\"computedDigests\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"digestType\": \"SHA256\",\n\t\t\t\t\t\t\t\"digest\": \"8EAFAE3305DB57A27CA5A261525515461CB7232A34A44AD96441B88BCA9B9849\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"digestType\": \"SHA384\",\n\t\t\t\t\t\t\t\"digest\": \"4A6DA59E91872B5B835FCEE5987B17151A6F10FE409B595BEEEDB28FE64315C9C268493B59A0BF72EA84BE0F20A33F96\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\",\n\t\t\t\t\"lastUsedOn\": \"0001-01-01T00:00:00\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"DNSKEY\",\n\t\t\t\t\"ttl\": 86400,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"flags\": \"ZoneKey\",\n\t\t\t\t\t\"protocol\": 3,\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"publicKey\": \"337uQ11fdKbr6sKYq9mwwBC2xdnu0geuIkfHcIauKNI= rKk7pfVKlLfcGBOIn5hEVeod2aIRIyUiivdTPzrmpIo=\",\n\t\t\t\t\t\"computedKeyTag\": 4811,\n\t\t\t\t\t\"dnsKeyState\": \"Published\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"example.com\",\n\t\t\t\t\"type\": \"NSEC3PARAM\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"hashAlgorithm\": \"SHA1\",\n\t\t\t\t\t\"flags\": \"None\",\n\t\t\t\t\t\"iterations\": 0,\n\t\t\t\t\t\"salt\": \"\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"*.example.com\",\n\t\t\t\t\"type\": \"A\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"ipAddress\": \"7.7.7.7\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"*.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"A\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 2,\n\t\t\t\t\t\"originalTtl\": 3600,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:25:35Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:25:35Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"ZoUNNEdb8XWqHHi5o4BcUe7deRVlJZLhQtc3sjRtuJ68DNPDmQ0GfCrNTigJcomspr7CYqWcXfoSOqu6f2AyyQ==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"4F3CNT8CU22TNGEC382JJ4GDE4RB47UB.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"NSEC3\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 3,\n\t\t\t\t\t\"originalTtl\": 900,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:45:31Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:45:31Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"piZeLYa6WpHyiJerPlXq2s+JKBjHznNALXHJCOfiQ4o/iTqWILoqYHfKB5AWrLwLmkxXcbKf63CnEMGlinRidg==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"4F3CNT8CU22TNGEC382JJ4GDE4RB47UB.example.com\",\n\t\t\t\t\"type\": \"NSEC3\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"hashAlgorithm\": \"SHA1\",\n\t\t\t\t\t\"flags\": \"None\",\n\t\t\t\t\t\"iterations\": 0,\n\t\t\t\t\t\"salt\": \"\",\n\t\t\t\t\t\"nextHashedOwnerName\": \"KG19N32806C832KIJDNGLQ8P9M2R5MDJ\",\n\t\t\t\t\t\"types\": [\n\t\t\t\t\t\t\"A\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"KG19N32806C832KIJDNGLQ8P9M2R5MDJ.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"NSEC3\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 3,\n\t\t\t\t\t\"originalTtl\": 900,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:45:31Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:45:31Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"i/PMxc1LFA9a8jLxju7SSpoY7y8aZYkAILcCRIxE3lTundPJmzFG0U9kve04kqT7+Klmzj3OzXnCvjTA54+DZA==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"KG19N32806C832KIJDNGLQ8P9M2R5MDJ.example.com\",\n\t\t\t\t\"type\": \"NSEC3\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"hashAlgorithm\": \"SHA1\",\n\t\t\t\t\t\"flags\": \"None\",\n\t\t\t\t\t\"iterations\": 0,\n\t\t\t\t\t\"salt\": \"\",\n\t\t\t\t\t\"nextHashedOwnerName\": \"MIFDNDT3NFF3OD53O7TLA1HRFF95JKUK\",\n\t\t\t\t\t\"types\": [\n\t\t\t\t\t\t\"NS\",\n\t\t\t\t\t\t\"DS\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"MIFDNDT3NFF3OD53O7TLA1HRFF95JKUK.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"NSEC3\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 3,\n\t\t\t\t\t\"originalTtl\": 900,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:45:31Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:45:31Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"mr37TDMmWJ3YLNtpYy++S9eAeHIXKajX6jB8zLscJyC1uI0OFnSTuesfhIlLDbj0SDgrzRQWsLmvMKzfq89TJA==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"MIFDNDT3NFF3OD53O7TLA1HRFF95JKUK.example.com\",\n\t\t\t\t\"type\": \"NSEC3\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"hashAlgorithm\": \"SHA1\",\n\t\t\t\t\t\"flags\": \"None\",\n\t\t\t\t\t\"iterations\": 0,\n\t\t\t\t\t\"salt\": \"\",\n\t\t\t\t\t\"nextHashedOwnerName\": \"ONIB9MGUB9H0RML3CDF5BGRJ59DKJHVK\",\n\t\t\t\t\t\"types\": [\n\t\t\t\t\t\t\"CNAME\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"ONIB9MGUB9H0RML3CDF5BGRJ59DKJHVK.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"NSEC3\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 3,\n\t\t\t\t\t\"originalTtl\": 900,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:45:31Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:45:31Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"GGh/KkB6C2D55xRJa0zFbZ8As3DZK9btUamryZVmyo7FaLPyltkeRZor9OExgQ6HC1SLXNGJIfCO9cM4K6P8iw==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"ONIB9MGUB9H0RML3CDF5BGRJ59DKJHVK.example.com\",\n\t\t\t\t\"type\": \"NSEC3\",\n\t\t\t\t\"ttl\": 900,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"hashAlgorithm\": \"SHA1\",\n\t\t\t\t\t\"flags\": \"None\",\n\t\t\t\t\t\"iterations\": 0,\n\t\t\t\t\t\"salt\": \"\",\n\t\t\t\t\t\"nextHashedOwnerName\": \"4F3CNT8CU22TNGEC382JJ4GDE4RB47UB\",\n\t\t\t\t\t\"types\": [\n\t\t\t\t\t\t\"A\",\n\t\t\t\t\t\t\"NS\",\n\t\t\t\t\t\t\"SOA\",\n\t\t\t\t\t\t\"DNSKEY\",\n\t\t\t\t\t\t\"NSEC3PARAM\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"sub.example.com\",\n\t\t\t\t\"type\": \"NS\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"nameServer\": \"server1\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"sub.example.com\",\n\t\t\t\t\"type\": \"DS\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"keyTag\": 46125,\n\t\t\t\t\t\"algorithm\": \"ECDSAP384SHA384\",\n\t\t\t\t\t\"digestType\": \"SHA1\",\n\t\t\t\t\t\"digest\": \"5590E425472785A16DC0F853000557DB5543C39E\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"sub.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"NS\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 3,\n\t\t\t\t\t\"originalTtl\": 3600,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:25:35Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:25:35Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"hFzYTL9V0/0UQZlvZpRWCOvu/2udvhswKoxpe4+quNuC6K59W7uCJLuDm/z0aFK5nW8Of4oTk2YjSBZo0nBSlg==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"sub.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"DS\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 3,\n\t\t\t\t\t\"originalTtl\": 3600,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T12:53:39Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T11:53:39Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"UYpUKV5Uq7DM3rltg3sPFOwYgRa2yBzT/j9U8xCh5oyXt27fIn3eemvqqe9qV4xeQaAN0QfQPkj9vmOZSAYafg==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"www.example.com\",\n\t\t\t\t\"type\": \"CNAME\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"cname\": \"example.com\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"name\": \"www.example.com\",\n\t\t\t\t\"type\": \"RRSIG\",\n\t\t\t\t\"ttl\": 3600,\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"typeCovered\": \"CNAME\",\n\t\t\t\t\t\"algorithm\": \"ECDSAP256SHA256\",\n\t\t\t\t\t\"labels\": 3,\n\t\t\t\t\t\"originalTtl\": 3600,\n\t\t\t\t\t\"signatureExpiration\": \"2022-03-15T11:25:35Z\",\n\t\t\t\t\t\"signatureInception\": \"2022-03-05T10:25:35Z\",\n\t\t\t\t\t\"keyTag\": 61009,\n\t\t\t\t\t\"signersName\": \"example.com\",\n\t\t\t\t\t\"signature\": \"cAbYvDJhZGLS/uI5I4mSrh7S5gEUy6bmX2sY7zEd1XVFPqrUOZHbVZuwXPjA6r9/m0rCaww9RiG90JhNNDLEtA==\"\n\t\t\t\t},\n\t\t\t\t\"dnssecStatus\": \"Unknown\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Update Record\n\nUpdates an existing record in an authoritative zone.\n\nURL:\\\n`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`\n\nOBSOLETE PATH:\\\n`/api/zone/updateRecord`\\\n`/api/updateRecord`\n\nPERMISSIONS:\\\nZones: None\\\nZone: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `domain`: The domain name of the zone to update the record.\n- `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used.\n- `type`: The type of the resource record to update.\n- `newDomain` (optional): The new domain name to be set for the record. To be used to rename sub domain name of the record.\n- `ttl` (optional): The TTL value of the resource record. Default value of `3600` is used when parameter is missing.\n- `disable` (optional): Specifies if the record should be disabled. The default value is `false` when this parameter is missing.\n- `comments` (optional): Sets comments for the updated resource record.\n- `expiryTtl` (optional): Set to automatically delete the record when the value in seconds elapses since the record’s last modified time.\n- `ipAddress` (optional): The current IP address in the `A` or `AAAA` record. This parameter is required when updating `A` or `AAAA` record.\n- `newIpAddress` (optional): The new IP address in the `A` or `AAAA` record. This parameter when missing will use the current value in the record.\n- `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.\n- `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.\n- `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.\n- `nameServer` (optional): The current name server domain name. This option is required for updating `NS` record.\n- `newNameServer` (optional): The new server domain name. This option is used for updating `NS` record.\n- `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.\n- `cname` (optional): The CNAME domain name to update in the existing `CNAME` record.\n- `primaryNameServer` (optional): This is the primary name server parameter in the SOA record. This parameter is required when updating the SOA record.\n- `responsiblePerson` (optional): This is the responsible person parameter in the SOA record. This parameter is required when updating the SOA record.\n- `serial` (optional): This is the serial parameter in the SOA record. This parameter is required when updating the SOA record.\n- `refresh` (optional): This is the refresh parameter in the SOA record. This parameter is required when updating the SOA record.\n- `retry` (optional): This is the retry parameter in the SOA record. This parameter is required when updating the SOA record.\n- `expire` (optional): This is the expire parameter in the SOA record. This parameter is required when updating the SOA record.\n- `minimum` (optional): This is the minimum parameter in the SOA record. This parameter is required when updating the SOA record.\n- `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.\n- `ptrName`(optional): The current PTR domain name. This option is required for updating `PTR` record.\n- `newPtrName`(optional): The new PTR domain name. This option is required for updating `PTR` record.\n- `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.\n- `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.\n- `exchange` (optional): The current exchange domain name. This option is required for updating `MX` record.\n- `newExchange` (optional): The new exchange domain name. This option is required for updating `MX` record.\n- `text` (optional): The current text value. This option is required for updating `TXT` record.\n- `newText` (optional): The new text value. This option is required for updating `TXT` record.\n- `splitText` (optional): The current split text value. This option is used for updating `TXT` record and is set to `false` when unspecified.\n- `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.\n- `mailbox` (optional): The current email address value. This option is required for updating `RP` record.\n- `newMailbox` (optional): The new email address value. This option is used for updating `RP` record and is set to the current value when unspecified.\n- `txtDomain` (optional): The current TXT record's domain name value. This option is required for updating `RP` record.\n- `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.\n- `priority` (optional): This is the current priority in the SRV record. This parameter is required when updating the `SRV` record.\n- `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.\n- `weight` (optional): This is the current weight in the SRV record. This parameter is required when updating the `SRV` record.\n- `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.\n- `port` (optional): This is the port parameter in the SRV record. This parameter is required when updating the `SRV` record.\n- `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.\n- `target` (optional): The current target value. This parameter is required when updating the `SRV` record.\n- `newTarget` (optional): The new target value. This parameter when missing will use the old value. This parameter is required when updating the `SRV` record.\n- `naptrOrder` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record.\n- `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.\n- `naptrPreference` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record.\n- `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.\n- `naptrFlags` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record.\n- `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.\n- `naptrServices` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record.\n- `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.\n- `naptrRegexp` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record.\n- `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.\n- `naptrReplacement` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record.\n- `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.\n- `dname` (optional): The DNAME domain name. This parameter is required when updating the `DNAME` record.\n- `keyTag` (optional): This parameter is required when updating `DS` record.\n- `newKeyTag` (optional): This parameter is required when updating `DS` record.\n- `algorithm` (optional): This parameter is required when updating `DS` record.\n- `newAlgorithm` (optional): This parameter is required when updating `DS` record.\n- `digestType` (optional): This parameter is required when updating `DS` record.\n- `newDigestType` (optional): This parameter is required when updating `DS` record.\n- `digest` (optional): This parameter is required when updating `DS` record.\n- `newDigest` (optional): This parameter is required when updating `DS` record.\n- `sshfpAlgorithm` (optional): This parameter is required when updating `SSHFP` record.\n- `newSshfpAlgorithm` (optional): This parameter is required when updating `SSHFP` record.\n- `sshfpFingerprintType` (optional): This parameter is required when updating `SSHFP` record.\n- `newSshfpFingerprintType` (optional): This parameter is required when updating `SSHFP` record.\n- `sshfpFingerprint` (optional): This parameter is required when updating `SSHFP` record.\n- `newSshfpFingerprint` (optional): This parameter is required when updating `SSHFP` record.\n- `tlsaCertificateUsage` (optional): This parameter is required when updating `TLSA` record.\n- `newTlsaCertificateUsage` (optional): This parameter is required when updating `TLSA` record.\n- `tlsaSelector` (optional): This parameter is required when updating `TLSA` record.\n- `newTlsaSelector` (optional): This parameter is required when updating `TLSA` record.\n- `tlsaMatchingType` (optional): This parameter is required when updating `TLSA` record.\n- `newTlsaMatchingType` (optional): This parameter is required when updating `TLSA` record.\n- `tlsaCertificateAssociationData` (optional): This parameter is required when updating `TLSA` record.\n- `newTlsaCertificateAssociationData` (optional): This parameter is required when updating `TLSA` record.\n- `svcPriority` (optional): The priority value for `SVCB` or `HTTPS` record. This parameter is required for updating `SCVB` or `HTTPS` record.\n- `newSvcPriority` (optional): The new priority value for `SVCB` or `HTTPS` record. This parameter when missing will use the old value. \n- `svcTargetName` (optional): The target domain name for `SVCB` or `HTTPS` record. This parameter is required for updating `SCVB` or `HTTPS` record.\n- `newSvcTargetName` (optional): The new target domain name for `SVCB` or `HTTPS` record. This parameter when missing will use the old value. \n- `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.\n- `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. \n- `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.\n- `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.\n- `uriPriority` (optional): The priority value for the `URI` record. This parameter is required for updating the `URI` record.\n- `newUriPriority` (optional): The new priority value for the `URI` record. This parameter when missing will use the old value.\n- `uriWeight` (optional): The weight value for the `URI` record. This parameter is required for updating the `URI` record.\n- `newUriWeight` (optional): The new weight value for the `URI` record. This parameter when missing will use the old value.\n- `uri` (optional): The URI value for the `URI` record. This parameter is required for updating the `URI` record.\n- `newUri` (optional): The new URI value for the `URI` record. This parameter when missing will use the old value.\n- `flags` (optional): This is the flags parameter in the `CAA` record. This parameter is required when updating the `CAA` record.\n- `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.\n- `tag` (optional): This is the tag parameter in the `CAA` record. This parameter is required when updating the `CAA` record.\n- `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.\n- `value` (optional): The current value in `CAA` record. This parameter is required when updating the `CAA` record.\n- `newValue` (optional): The new value in `CAA` record. This parameter is required when updating the `CAA` record.\n- `aname` (optional): The current `ANAME` domain name. This parameter is required when updating the `ANAME` record.\n- `newAName` (optional): The new `ANAME` domain name. This parameter is required when updating the `ANAME` record.\n- `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.\n- `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.\n- `forwarder` (optional): The current forwarder address. This parameter is required when updating the `FWD` record.\n- `newForwarder` (optional): The new forwarder address. This parameter is required when updating the `FWD` record.\n- `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.\n- `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`.\n- `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.\n- `proxyAddress` (optional): The proxy server address to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `proxyPort` (optional): The proxy server port to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `proxyUsername` (optional): The proxy server username to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `proxyPassword` (optional): The proxy server password to use when `proxyType` is configured. This optional parameter is to be used with FWD records.\n- `appName` (optional): This parameter is required for updating the `APP` record.\n- `classPath` (optional): This parameter is required for updating the `APP` record.\n- `recordData` (optional): This parameter is used for updating the `APP` record as per the DNS app requirements.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"zone\": {\n\t\t\t\"name\": \"example.com\",\n\t\t\t\"type\": \"Primary\",\n\t\t\t\"internal\": false,\n\t\t\t\"dnssecStatus\": \"SignedWithNSEC\",\n\t\t\t\"disabled\": false\n\t\t},\n\t\t\"updatedRecord\": {\n\t\t\t\"disabled\": false,\n\t\t\t\"name\": \"example.com\",\n\t\t\t\"type\": \"SOA\",\n\t\t\t\"ttl\": 900,\n\t\t\t\"rData\": {\n\t\t\t\t\"primaryNameServer\": \"server1.home\",\n\t\t\t\t\"responsiblePerson\": \"hostadmin.example.com\",\n\t\t\t\t\"serial\": 75,\n\t\t\t\t\"refresh\": 900,\n\t\t\t\t\"retry\": 300,\n\t\t\t\t\"expire\": 604800,\n\t\t\t\t\"minimum\": 900\n\t\t\t},\n\t\t\t\"dnssecStatus\": \"Unknown\",\n\t\t\t\"lastUsedOn\": \"0001-01-01T00:00:00\"\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Record\n\nDeletes a record from an authoritative zone.\n\nURL:\\\n`http://localhost:5380/api/zones/records/delete?token=x&domain=example.com&zone=example.com&type=A&value=127.0.0.1`\n\nOBSOLETE PATH:\\\n`/api/zone/deleteRecord`\\\n`/api/deleteRecord`\n\nPERMISSIONS:\\\nZones: None\\\nZone: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `domain`: The domain name of the zone to delete the record.\n- `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used.\n- `type`: The type of the resource record to delete.\n- `ipAddress` (optional): This parameter is required when deleting `A` or `AAAA` record.\n- `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.\n- `nameServer` (optional): This parameter is required when deleting `NS` record.\n- `ptrName` (optional): This parameter is required when deleting `PTR` record.\n- `preference` (optional): This parameter is required when deleting `MX` record.\n- `exchange` (optional): This parameter is required when deleting `MX` record.\n- `text` (optional): This parameter is required when deleting `TXT` record.\n- `splitText` (optional): This parameter is used when deleting `TXT` record. Default value is set to `false` when unspecified.\n- `mailbox` (optional): Set an email address for deleting `RP` record.\n- `txtDomain` (optional): Set a `TXT` record's domain name for deleting `RP` record.\n- `priority` (optional): This parameter is required when deleting the `SRV` record.\n- `weight` (optional): This parameter is required when deleting the `SRV` record.\n- `port` (optional): This parameter is required when deleting the `SRV` record.\n- `target` (optional): This parameter is required when deleting the `SRV` record.\n- `naptrOrder` (optional): This parameter is required when deleting the `NAPTR` record.\n- `naptrPreference` (optional): This parameter is required when deleting the `NAPTR` record.\n- `naptrFlags` (optional): This parameter is required when deleting the `NAPTR` record.\n- `naptrServices` (optional): This parameter is required when deleting the `NAPTR` record.\n- `naptrRegexp` (optional): This parameter is required when deleting the `NAPTR` record.\n- `naptrReplacement` (optional): This parameter is required when deleting the `NAPTR` record.\n- `keyTag` (optional): This parameter is required when deleting `DS` record.\n- `algorithm` (optional): This parameter is required when deleting `DS` record.\n- `digestType` (optional): This parameter is required when deleting `DS` record.\n- `digest` (optional): This parameter is required when deleting `DS` record.\n- `sshfpAlgorithm` (optional): This parameter is required when deleting `SSHFP` record.\n- `sshfpFingerprintType` (optional): This parameter is required when deleting `SSHFP` record.\n- `sshfpFingerprint` (optional): This parameter is required when deleting `SSHFP` record.\n- `tlsaCertificateUsage` (optional): This parameter is required when deleting `TLSA` record.\n- `tlsaSelector` (optional): This parameter is required when deleting `TLSA` record.\n- `tlsaMatchingType` (optional): This parameter is required when deleting `TLSA` record.\n- `tlsaCertificateAssociationData` (optional): This parameter is required when deleting `TLSA` record.\n- `svcPriority` (optional): The priority value for `SVCB` or `HTTPS` record. This parameter is required for deleting `SCVB` or `HTTPS` record.\n- `svcTargetName` (optional): The target domain name for `SVCB` or `HTTPS` record. This parameter is required for deleting `SCVB` or `HTTPS` record.\n- `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.\n- `uriPriority` (optional): The priority value in the `URI` record. This parameter is required when deleting the `URI` record.\n- `uriWeight` (optional): The weight value in the `URI` record. This parameter is required when deleting the `URI` record.\n- `uri` (optional): The URI value in the `URI` record. This parameter is required when deleting the `URI` record.\n- `flags` (optional): This is the flags parameter in the `CAA` record. This parameter is required when deleting the `CAA` record.\n- `tag` (optional): This is the tag parameter in the `CAA` record. This parameter is required when deleting the `CAA` record.\n- `value` (optional): This parameter is required when deleting the `CAA` record.\n- `aname` (optional): This parameter is required when deleting the `ANAME` record.\n- `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.\n- `forwarder` (optional): This parameter is required when deleting the `FWD` record.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n## DNS Cache API Calls\n\nThese API calls allow managing the DNS server cache.\n\n### List Cached Zones\n\nList all cached zones.\n\nURL:\\\n`http://localhost:5380/api/cache/list?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/listCachedZones`\n\nPERMISSIONS:\\\nCache: View \n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `domain` (Optional): The domain name to list records. If not passed, the domain is set to empty string which corresponds to the zone root.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"domain\": \"google.com\",\n\t\t\"zones\": [],\n\t\t\"records\": [\n\t\t\t{\n\t\t\t\t\"name\": \"google.com\",\n\t\t\t\t\"type\": \"A\",\n\t\t\t\t\"ttl\": \"283 (4 mins 43 sec)\",\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"value\": \"216.58.199.174\"\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Cached Zone\n\nDeletes a specific zone from the DNS cache.\n\nURL:\\\n`http://localhost:5380/api/cache/delete?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/deleteCachedZone`\n\nPERMISSIONS:\\\nCache: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `domain`: The domain name to delete cached records from.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Flush DNS Cache\n\nThis call clears all the DNS cache from the server forcing the DNS server to make recursive queries again to populate the cache.\n\nURL:\\\n`http://localhost:5380/api/cache/flush?token=x`\n\nOBSOLETE PATH:\\\n`/api/flushDnsCache`\n\nPERMISSIONS:\\\nCache: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n## Allowed Zones API Calls\n\nThese API calls allow managing the Allowed zones.\n\n### List Allowed Zones\n\nList all allowed zones.\n\nURL:\\\n`http://localhost:5380/api/allowed/list?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/listAllowedZones`\n\nPERMISSIONS:\\\nAllowed: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `domain` (Optional): The domain name to list records. If not passed, the domain is set to empty string which corresponds to the zone root.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"domain\": \"google.com\",\n\t\t\"zones\": [],\n\t\t\"records\": [\n\t\t\t{\n\t\t\t\t\"name\": \"google.com\",\n\t\t\t\t\"type\": \"NS\",\n\t\t\t\t\"ttl\": \"14400 (4 hours)\",\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"value\": \"server1\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"google.com\",\n\t\t\t\t\"type\": \"SOA\",\n\t\t\t\t\"ttl\": \"14400 (4 hours)\",\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"primaryNameServer\": \"server1\",\n\t\t\t\t\t\"responsiblePerson\": \"hostadmin.server1\",\n\t\t\t\t\t\"serial\": 1,\n\t\t\t\t\t\"refresh\": 14400,\n\t\t\t\t\t\"retry\": 3600,\n\t\t\t\t\t\"expire\": 604800,\n\t\t\t\t\t\"minimum\": 900\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Allow Zone\n\nAdds a domain name into the Allowed Zones.\n\nURL:\\\n`http://localhost:5380/api/allowed/add?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/allowZone`\n\nPERMISSIONS:\\\nAllowed: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `domain`: The domain name for the zone to be added.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Allowed Zone\n\nAllows deleting a zone from the Allowed Zones.\n\nURL:\\\n`http://localhost:5380/api/allowed/delete?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/deleteAllowedZone`\n\nPERMISSIONS:\\\nAllowed: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `domain`: The domain name for the zone to be deleted.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Flush Allowed Zone\n\nFlushes the Allowed zone to clear all records.\n\nURL:\\\n`http://localhost:5380/api/allowed/flush?token=x`\n\nOBSOLETE PATH:\\\n`/api/flushAllowedZone`\n\nPERMISSIONS:\\\nAllowed: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Import Allowed Zones\n\nImports domain names into the Allowed Zones.\n\nURL:\\\n`http://localhost:5380/api/allowed/import?token=x`\n\nOBSOLETE PATH:\\\n`/api/importAllowedZones`\n\nPERMISSIONS:\\\nAllowed: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nREQUEST:\nThis 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:\n\n```\nallowedZones=google.com,twitter.com\n```\n\nWHERE:\n- `allowedZones`: A list of comma separated domain names that are to be imported.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Export Allowed Zones\n\nAllows exporting all the zones from the Allowed Zones as a text file.\n\nURL:\\\n`http://localhost:5380/api/allowed/export?token=x`\n\nOBSOLETE PATH:\\\n`/api/exportAllowedZones`\n\nPERMISSIONS:\\\nAllowed: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\nResponse is a downloadable text file with `Content-Type: text/plain` and `Content-Disposition: attachment`.\n\n## Blocked Zones API Calls\n\nThese API calls allow managing the Blocked zones.\n\n### List Blocked Zones\n\nList all blocked zones.\n\nURL:\\\n`http://localhost:5380/api/blocked/list?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/listBlockedZones`\n\nPERMISSIONS:\\\nBlocked: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `domain` (Optional): The domain name to list records. If not passed, the domain is set to empty string which corresponds to the zone root.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"domain\": \"google.com\",\n\t\t\"zones\": [],\n\t\t\"records\": [\n\t\t\t{\n\t\t\t\t\"name\": \"google.com\",\n\t\t\t\t\"type\": \"NS\",\n\t\t\t\t\"ttl\": \"14400 (4 hours)\",\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"value\": \"server1\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"google.com\",\n\t\t\t\t\"type\": \"SOA\",\n\t\t\t\t\"ttl\": \"14400 (4 hours)\",\n\t\t\t\t\"rData\": {\n\t\t\t\t\t\"primaryNameServer\": \"server1\",\n\t\t\t\t\t\"responsiblePerson\": \"hostadmin.server1\",\n\t\t\t\t\t\"serial\": 1,\n\t\t\t\t\t\"refresh\": 14400,\n\t\t\t\t\t\"retry\": 3600,\n\t\t\t\t\t\"expire\": 604800,\n\t\t\t\t\t\"minimum\": 900\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Block Zone\n\nAdds a domain name into the Blocked Zones.\n\nURL:\\\n`http://localhost:5380/api/blocked/add?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/blockZone`\n\nPERMISSIONS:\\\nBlocked: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `domain`: The domain name for the zone to be added.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Blocked Zone\n\nAllows deleting a zone from the Blocked Zones.\n\nURL:\\\n`http://localhost:5380/api/blocked/delete?token=x&domain=google.com`\n\nOBSOLETE PATH:\\\n`/api/deleteBlockedZone`\n\nPERMISSIONS:\\\nBlocked: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `domain`: The domain name for the zone to be deleted.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Flush Blocked Zone\n\nFlushes the Blocked zone to clear all records.\n\nURL:\\\n`http://localhost:5380/api/blocked/flush?token=x`\n\nOBSOLETE PATH:\\\n`/api/flushBlockedZone`\n\nPERMISSIONS:\\\nBlocked: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Import Blocked Zones\n\nImports domain names into Blocked Zones.\n\nURL:\\\n`http://localhost:5380/api/blocked/import?token=x`\n\nOBSOLETE PATH:\\\n`/api/importBlockedZones`\n\nPERMISSIONS:\\\nBlocked: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nREQUEST:\nThis 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:\n\n```\nblockedZones=google.com,twitter.com\n```\n\nWHERE:\n- `blockedZones`: A list of comma separated domain names that are to be imported.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Export Blocked Zones\n\nAllows exporting all the zones from the Blocked Zones as a text file.\n\nURL:\\\n`http://localhost:5380/api/blocked/export?token=x`\n\nOBSOLETE PATH:\\\n`/api/exportBlockedZones`\n\nPERMISSIONS:\\\nBlocked: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\nResponse is a downloadable text file with `Content-Type: text/plain` and `Content-Disposition: attachment`.\n\n## DNS Apps API Calls\n\nThese API calls allows managing DNS Apps.\n\n### List Apps\n\nLists 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.\n\nURL:\\\n`http://localhost:5380/api/apps/list?token=x`\n\nPERMISSIONS:\\\nApps/Zones/Logs: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"apps\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Block Page\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"dnsApps\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"classPath\": \"BlockPageWebServer.App\",\n\t\t\t\t\t\t\"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.\",\n\t\t\t\t\t\t\"isAppRecordRequestHandler\": false,\n\t\t\t\t\t\t\"isRequestController\": false,\n\t\t\t\t\t\t\"isAuthoritativeRequestHandler\": false,\n\t\t\t\t\t\t\"isRequestBlockingHandler\": false,\n\t\t\t\t\t\t\"isQueryLogger\": false,\n\t\t\t\t\t\t\"isPostProcessor\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"What Is My DNS\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"dnsApps\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"classPath\": \"WhatIsMyDns.App\",\n\t\t\t\t\t\t\"description\": \"Returns the IP address of the user's DNS Server for A, AAAA, and TXT queries.\",\n\t\t\t\t\t\t\"isAppRecordRequestHandler\": true,\n\t\t\t\t\t\t\"recordDataTemplate\": null,\n\t\t\t\t\t\t\"isRequestController\": false,\n\t\t\t\t\t\t\"isAuthoritativeRequestHandler\": false,\n\t\t\t\t\t\t\"isRequestBlockingHandler\": false,\n\t\t\t\t\t\t\"isQueryLogger\": false,\n\t\t\t\t\t\t\"isPostProcessor\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### List Store Apps\n\nLists all available apps on the DNS App Store.\n\nURL:\\\n`http://localhost:5380/api/apps/listStoreApps?token=x`\n\nPERMISSIONS:\\\nApps: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"storeApps\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Geo Continent\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"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.\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp.zip\",\n\t\t\t\t\"size\": \"2.01 MB\",\n\t\t\t\t\"installed\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"Geo Country\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"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.\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp.zip\",\n\t\t\t\t\"size\": \"2.01 MB\",\n\t\t\t\t\"installed\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"Geo Distance\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"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.\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp.zip\",\n\t\t\t\t\"size\": \"28.6 MB\",\n\t\t\t\t\"installed\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"Split Horizon\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"description\": \"Returns different set of A or AAAA records, or CNAME record for clients querying over public and private networks.\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp.zip\",\n\t\t\t\t\"size\": \"11.1 KB\",\n\t\t\t\t\"installed\": true,\n\t\t\t\t\"installedVersion\": \"1.1\",\n\t\t\t\t\"updateAvailable\": false\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"What Is My Dns\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"description\": \"Returns the IP address of the user's DNS Server for A, AAAA, and TXT queries.\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp.zip\",\n\t\t\t\t\"size\": \"8.79 KB\",\n\t\t\t\t\"installed\": true,\n\t\t\t\t\"installedVersion\": \"1.1\",\n\t\t\t\t\"updateAvailable\": false\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Download And Install App\n\nDownload an app zip file from given URL and installs it on the DNS Server.\n\nURL:\\\n`http://localhost:5380/api/apps/downloadAndInstall?token=x&name=app-name&url=https://example.com/app.zip`\n\nPERMISSIONS:\\\nApps: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the app to install.\n- `url`: The URL of the app zip file. URL must start with `https://`.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"installedApp\": {\n\t\t\t\"name\": \"Wild IP\",\n\t\t\t\"version\": \"1.0\",\n\t\t\t\"dnsApps\": [\n\t\t\t\t{\n\t\t\t\t\t\"classPath\": \"WildIp.App\",\n\t\t\t\t\t\"description\": \"Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.\",\n\t\t\t\t\t\"isAppRecordRequestHandler\": true,\n\t\t\t\t\t\"recordDataTemplate\": null,\n\t\t\t\t\t\"isRequestController\": false,\n\t\t\t\t\t\"isAuthoritativeRequestHandler\": false,\n\t\t\t\t\t\"isRequestBlockingHandler\": false,\n\t\t\t\t\t\"isQueryLogger\": false,\n\t\t\t\t\t\"isPostProcessor\": false\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Download And Update App\n\nDownload an app zip file from given URL and updates an existing app installed on the DNS Server.\n\nURL:\\\n`http://localhost:5380/api/apps/downloadAndUpdate?token=x&name=app-name&url=https://example.com/app.zip`\n\nPERMISSIONS:\\\nApps: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the app to install.\n- `url`: The URL of the app zip file. URL must start with `https://`.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"updatedApp\": {\n\t\t\t\"name\": \"Wild IP\",\n\t\t\t\"version\": \"1.0\",\n\t\t\t\"dnsApps\": [\n\t\t\t\t{\n\t\t\t\t\t\"classPath\": \"WildIp.App\",\n\t\t\t\t\t\"description\": \"Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.\",\n\t\t\t\t\t\"isAppRecordRequestHandler\": true,\n\t\t\t\t\t\"recordDataTemplate\": null,\n\t\t\t\t\t\"isRequestController\": false,\n\t\t\t\t\t\"isAuthoritativeRequestHandler\": false,\n\t\t\t\t\t\"isRequestBlockingHandler\": false,\n\t\t\t\t\t\"isQueryLogger\": false,\n\t\t\t\t\t\"isPostProcessor\": false\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Install App\n\nInstalls a DNS application on the DNS server.\n\nURL:\\\n`http://localhost:5380/api/apps/install?token=x&name=app-name`\n\nPERMISSIONS:\\\nApps: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the app to install.\n\nREQUEST: 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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"installedApp\": {\n\t\t\t\"name\": \"Wild IP\",\n\t\t\t\"version\": \"1.0\",\n\t\t\t\"dnsApps\": [\n\t\t\t\t{\n\t\t\t\t\t\"classPath\": \"WildIp.App\",\n\t\t\t\t\t\"description\": \"Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.\",\n\t\t\t\t\t\"isAppRecordRequestHandler\": true,\n\t\t\t\t\t\"recordDataTemplate\": null,\n\t\t\t\t\t\"isRequestController\": false,\n\t\t\t\t\t\"isAuthoritativeRequestHandler\": false,\n\t\t\t\t\t\"isRequestBlockingHandler\": false,\n\t\t\t\t\t\"isQueryLogger\": false,\n\t\t\t\t\t\"isPostProcessor\": false\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Update App\n\nAllows to manually update an installed app using a provided app zip file.\n\nURL:\\\n`http://localhost:5380/api/apps/update?token=x&name=app-name`\n\nPERMISSIONS:\\\nApps: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the app to update.\n\nREQUEST: 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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"updatedApp\": {\n\t\t\t\"name\": \"Wild IP\",\n\t\t\t\"version\": \"1.0\",\n\t\t\t\"dnsApps\": [\n\t\t\t\t{\n\t\t\t\t\t\"classPath\": \"WildIp.App\",\n\t\t\t\t\t\"description\": \"Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.\",\n\t\t\t\t\t\"isAppRecordRequestHandler\": true,\n\t\t\t\t\t\"recordDataTemplate\": null,\n\t\t\t\t\t\"isRequestController\": false,\n\t\t\t\t\t\"isAuthoritativeRequestHandler\": false,\n\t\t\t\t\t\"isRequestBlockingHandler\": false,\n\t\t\t\t\t\"isQueryLogger\": false,\n\t\t\t\t\t\"isPostProcessor\": false\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Uninstall App\n\nUninstall an app from the DNS server. This does not remove any APP records that were using this DNS application.\n\nURL:\\\n`http://localhost:5380/api/apps/uninstall?token=x&name=app-name`\n\nPERMISSIONS:\\\nApps: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the app to uninstall.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Get App Config\n\nRetrieve the DNS application config from the `dnsApp.config` file in the application folder.\n\nURL:\\\n`http://localhost:5380/api/apps/config/get?token=x&name=app-name`\n\nOBSOLETE PATH:\\\n`/api/apps/getConfig`\n\nPERMISSIONS:\\\nApps: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `name`: The name of the app to retrieve the config.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"config\": \"config data or `null`\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set App Config\n\nSaves the provided DNS application config into the `dnsApp.config` file in the application folder.\n\nURL:\\\n`http://localhost:5380/api/apps/config/set?token=x&name=app-name`\n\nOBSOLETE PATH:\\\n`/api/apps/setConfig`\n\nPERMISSIONS:\\\nApps: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the app to retrieve the config.\n\nREQUEST: 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:\n```\nconfig=query-string-encoded-config-data\n```\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n## DNS Client API Calls\n\nThese API calls allow interacting with the DNS Client section.\n\n### Resolve Query\n\nURL:\\\n`http://localhost:5380/api/dnsClient/resolve?token=x&server=this-server&domain=example.com&type=A&protocol=UDP`\n\nOBSOLETE PATH:\\\n`/api/resolveQuery`\n\nPERMISSIONS:\\\nDnsClient: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `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.\n- `domain`: The domain name to query.\n- `type`: The type of the query.\n- `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.\n- `dnssec` (optional): Set to `true` to enable DNSSEC validation.\n- `eDnsClientSubnet` (optional): The network address to be used with EDNS Client Subnet option in the request.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"result\": {\n\t\t\t\"Metadata\": {\n\t\t\t\t\"NameServer\": \"server1:53 (127.0.0.1:53)\",\n\t\t\t\t\"Protocol\": \"Udp\",\n\t\t\t\t\"DatagramSize\": \"45 bytes\",\n\t\t\t\t\"RoundTripTime\": \"1.42 ms\"\n\t\t\t},\n\t\t\t\"Identifier\": 60127,\n\t\t\t\"IsResponse\": true,\n\t\t\t\"OPCODE\": \"StandardQuery\",\n\t\t\t\"AuthoritativeAnswer\": true,\n\t\t\t\"Truncation\": false,\n\t\t\t\"RecursionDesired\": true,\n\t\t\t\"RecursionAvailable\": true,\n\t\t\t\"Z\": 0,\n\t\t\t\"AuthenticData\": false,\n\t\t\t\"CheckingDisabled\": false,\n\t\t\t\"RCODE\": \"NoError\",\n\t\t\t\"QDCOUNT\": 1,\n\t\t\t\"ANCOUNT\": 1,\n\t\t\t\"NSCOUNT\": 0,\n\t\t\t\"ARCOUNT\": 0,\n\t\t\t\"Question\": [\n\t\t\t\t{\n\t\t\t\t\t\"Name\": \"example.com\",\n\t\t\t\t\t\"Type\": \"A\",\n\t\t\t\t\t\"Class\": \"IN\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"Answer\": [\n\t\t\t\t{\n\t\t\t\t\t\"Name\": \"example.com\",\n\t\t\t\t\t\"Type\": \"A\",\n\t\t\t\t\t\"Class\": \"IN\",\n\t\t\t\t\t\"TTL\": \"86400 (1 day)\",\n\t\t\t\t\t\"RDLENGTH\": \"4 bytes\",\n\t\t\t\t\t\"RDATA\": {\n\t\t\t\t\t\t\"IPAddress\": \"127.0.0.1\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"Authority\": [],\n\t\t\t\"Additional\": []\n\t\t},\n\t\t\"rawResponses\": []\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n## Settings API Calls\n\nThese API calls allow managing the DNS server settings.\n\n### Get DNS Settings\n\nThis call returns all the DNS server settings.\n\nURL:\\\n`http://localhost:5380/api/settings/get?token=x`\n\nOBSOLETE PATH:\\\n`/api/getDnsSettings`\n\nPERMISSIONS:\\\nSettings: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"version\": \"14.3\",\n\t\t\"uptimestamp\": \"2025-05-31T10:28:21.6864142Z\",\n\t\t\"dnsServerDomain\": \"server1\",\n\t\t\"dnsServerLocalEndPoints\": [\n\t\t\t\"0.0.0.0:53\",\n\t\t\t\"[::]:53\"\n\t\t],\n\t\t\"dnsServerIPv4SourceAddresses\": [\n\t\t\t\"0.0.0.0\"\n\t\t],\n\t\t\"dnsServerIPv6SourceAddresses\": [\n\t\t\t\"::\"\n\t\t],\n\t\t\"defaultRecordTtl\": 3600,\n\t\t\"defaultNsRecordTtl\": 14400,\n\t\t\"defaultSoaRecordTtl\": 900,\n\t\t\"defaultResponsiblePerson\": null,\n\t\t\"useSoaSerialDateScheme\": false,\n\t\t\"minSoaRefresh\": 300,\n\t\t\"minSoaRetry\": 300,\n\t\t\"zoneTransferAllowedNetworks\": [],\n\t\t\"notifyAllowedNetworks\": [],\n\t\t\"dnsAppsEnableAutomaticUpdate\": true,\n\t\t\"preferIPv6\": false,\n\t\t\"enableUdpSocketPool\": false,\n\t\t\"socketPoolExcludedPorts\": [\n\t\t\t53443\n\t\t],\n\t\t\"udpPayloadSize\": 1232,\n\t\t\"dnssecValidation\": true,\n\t\t\"eDnsClientSubnet\": false,\n\t\t\"eDnsClientSubnetIPv4PrefixLength\": 24,\n\t\t\"eDnsClientSubnetIPv6PrefixLength\": 56,\n\t\t\"eDnsClientSubnetIpv4Override\": null,\n\t\t\"eDnsClientSubnetIpv6Override\": null,\n\t\t\"qpmPrefixLimitsIPv4\": [\n\t\t\t{\n\t\t\t\t\"prefix\": 32,\n\t\t\t\t\"udpLimit\": 600,\n\t\t\t\t\"tcpLimit\": 600\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"prefix\": 24,\n\t\t\t\t\"udpLimit\": 6000,\n\t\t\t\t\"tcpLimit\": 6000\n\t\t\t}\n\t\t],\n\t\t\"qpmPrefixLimitsIPv6\": [\n\t\t\t{\n\t\t\t\t\"prefix\": 128,\n\t\t\t\t\"udpLimit\": 600,\n\t\t\t\t\"tcpLimit\": 600\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"prefix\": 64,\n\t\t\t\t\"udpLimit\": 1200,\n\t\t\t\t\"tcpLimit\": 1200\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"prefix\": 56,\n\t\t\t\t\"udpLimit\": 6000,\n\t\t\t\t\"tcpLimit\": 6000\n\t\t\t}\n\t\t],\n\t\t\"qpmLimitSampleMinutes\": 5,\n\t\t\"qpmLimitUdpTruncationPercentage\": 50,\n\t\t\"qpmLimitBypassList\": [],\n\t\t\"clientTimeout\": 2000,\n\t\t\"tcpSendTimeout\": 10000,\n\t\t\"tcpReceiveTimeout\": 10000,\n\t\t\"quicIdleTimeout\": 60000,\n\t\t\"quicMaxInboundStreams\": 100,\n\t\t\"listenBacklog\": 100,\n\t\t\"maxConcurrentResolutionsPerCore\": 100,\n\t\t\"webServiceLocalAddresses\": [\n\t\t\t\"[::]\"\n\t\t],\n\t\t\"webServiceHttpPort\": 5380,\n\t\t\"webServiceEnableTls\": true,\n\t\t\"webServiceEnableHttp3\": false,\n\t\t\"webServiceHttpToTlsRedirect\": false,\n\t\t\"webServiceUseSelfSignedTlsCertificate\": true,\n\t\t\"webServiceTlsPort\": 53443,\n\t\t\"webServiceTlsCertificatePath\": null,\n\t\t\"webServiceTlsCertificatePassword\": \"************\",\n\t\t\"webServiceRealIpHeader\": \"X-Real-IP\",\n\t\t\"enableDnsOverUdpProxy\": false,\n\t\t\"enableDnsOverTcpProxy\": false,\n\t\t\"enableDnsOverHttp\": false,\n\t\t\"enableDnsOverTls\": false,\n\t\t\"enableDnsOverHttps\": false,\n\t\t\"enableDnsOverHttp3\": false,\n\t\t\"enableDnsOverQuic\": false,\n\t\t\"dnsOverUdpProxyPort\": 538,\n\t\t\"dnsOverTcpProxyPort\": 538,\n\t\t\"dnsOverHttpPort\": 80,\n\t\t\"dnsOverTlsPort\": 853,\n\t\t\"dnsOverHttpsPort\": 443,\n\t\t\"dnsOverQuicPort\": 853,\n\t\t\"reverseProxyNetworkACL\": [],\n\t\t\"dnsTlsCertificatePath\": null,\n\t\t\"dnsTlsCertificatePassword\": \"************\",\n\t\t\"dnsOverHttpRealIpHeader\": \"X-Real-IP\",\n\t\t\"tsigKeys\": [\n\t\t\t{\n\t\t\t\t\"keyName\": \"home\",\n\t\t\t\t\"sharedSecret\": \"E9crgbHbzgEI+e+/pBmARRif70ScKf2sc/FjrgnCWyc=\",\n\t\t\t\t\"algorithmName\": \"hmac-sha256\"\n\t\t\t}\n\t\t],\n\t\t\"recursion\": \"AllowOnlyForPrivateNetworks\",\n\t\t\"recursionNetworkACL\": [],\n\t\t\"randomizeName\": false,\n\t\t\"qnameMinimization\": true,\n\t\t\"resolverRetries\": 2,\n\t\t\"resolverTimeout\": 1500,\n\t\t\"resolverConcurrency\": 2,\n\t\t\"resolverMaxStackCount\": 16,\n\t\t\"saveCache\": true,\n\t\t\"serveStale\": true,\n\t\t\"serveStaleTtl\": 259200,\n\t\t\"serveStaleAnswerTtl\": 30,\n\t\t\"serveStaleResetTtl\": 30,\n\t\t\"serveStaleMaxWaitTime\": 1800,\n\t\t\"cacheMaximumEntries\": 10000,\n\t\t\"cacheMinimumRecordTtl\": 10,\n\t\t\"cacheMaximumRecordTtl\": 604800,\n\t\t\"cacheNegativeRecordTtl\": 300,\n\t\t\"cacheFailureRecordTtl\": 10,\n\t\t\"cachePrefetchEligibility\": 2,\n\t\t\"cachePrefetchTrigger\": 9,\n\t\t\"cachePrefetchSampleIntervalInMinutes\": 5,\n\t\t\"cachePrefetchSampleEligibilityHitsPerHour\": 30,\n\t\t\"enableBlocking\": true,\n\t\t\"allowTxtBlockingReport\": true,\n\t\t\"blockingBypassList\": [],\n\t\t\"blockingType\": \"NxDomain\",\n\t\t\"blockingAnswerTtl\": 30,\n\t\t\"customBlockingAddresses\": [\n\t\t\t\"127.0.0.1\"\n\t\t],\n\t\t\"blockListUrls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\",\n\t\t\t\"https://big.oisd.nl/\"\n\t\t],\n\t\t\"blockListUpdateIntervalHours\": 24,\n\t\t\"blockListNextUpdatedOn\": \"2024-02-01T20:15:08.658124Z\",\n\t\t\"proxy\": null,\n\t\t\"forwarders\": null,\n\t\t\"forwarderProtocol\": \"Udp\",\n\t\t\"concurrentForwarding\": true,\n\t\t\"forwarderRetries\": 3,\n\t\t\"forwarderTimeout\": 2000,\n\t\t\"forwarderConcurrency\": 2,\n\t\t\"enableLogging\": true,\n\t\t\"loggingType\": \"File\",\n\t\t\"ignoreResolverLogs\": false,\n\t\t\"logQueries\": false,\n\t\t\"useLocalTime\": false,\n\t\t\"logFolder\": \"logs\",\n\t\t\"maxLogFileDays\": 30,\n\t\t\"enableInMemoryStats\": false,\n\t\t\"maxStatFileDays\": 365\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set DNS Settings\n\nThis call allows to change the DNS server settings. \n\nNote! 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.\n\nURL:\\\n`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`\n\nOBSOLETE PATH:\\\n`/api/setDnsSettings`\n\nPERMISSIONS:\\\nSettings: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `dnsServerDomain` (optional): The primary domain name used by this DNS Server to identify itself.\n- `dnsServerLocalEndPoints` (optional): Local end points are the network interface IP addresses and ports you want the DNS Server to listen for requests. \n- `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.\n- `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.\n- `defaultRecordTtl` (optional, cluster parameter): The default TTL value to use if not specified when adding or updating records in a Zone.\n- `defaultNsRecordTtl` (optional, cluster parameter): The default TTL value to use if not specified when adding or updating NS records in a Primary Zone.\n- `defaultSoaRecordTtl` (optional, cluster parameter): The default TTL value to use if not specified when adding or updating SOA records in a Primary Zone.\n- `defaultResponsiblePerson` (optional, cluster parameter): The default SOA Responsible Person email address to use when adding a Primary Zone.\n- `useSoaSerialDateScheme` (optional, cluster parameter): The default SOA Serial option to use if not specified when adding a Primary Zone.\n- `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`.\n- `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`.\n- `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.\n- `notifyAllowedNetworks` (optional, cluster parameter): A comma separated list of IP addresses or network addresses that are allowed to Notify all secondary zones.\n- `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.\n- `preferIPv6` (optional): DNS Server will use IPv6 for querying whenever possible with this option enabled. Initial value is `false`.\n- `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.\n- `socketPoolExcludedPorts` (optional): A comma separated list of port numbers that must be excluded from being used by the UDP socket pool.\n- `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`.\n- `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.\n- `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.\n- `eDnsClientSubnetIPv4PrefixLength` (optional, cluster parameter): The EDNS Client Subnet IPv4 prefix length to define the client subnet. Initial value is `24`.\n- `eDnsClientSubnetIPv6PrefixLength` (optional, cluster parameter): The EDNS Client Subnet IPv6 prefix length to define the client subnet. Initial value is `56`.\n- `eDnsClientSubnetIpv4Override` (optional, cluster parameter): The IPv4 network address that must be used as ECS for all outbound requests overriding client's actual subnet.\n- `eDnsClientSubnetIpv6Override` (optional, cluster parameter): The IPv6 network address that must be used as ECS for all outbound requests overriding client's actual subnet.\n- `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. \n- `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. \n- `qpmLimitSampleMinutes` (optional, cluster parameter): Sets the client query stats sample size in minutes for QPM limit feature. Initial value is `5`.\n- `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`.\n- `qpmLimitBypassList` (optional, cluster parameter): A comma separated list of IP addresses or network addresses that are allowed to bypass the QPM limit.\n- `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`.\n- `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`.\n- `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`.\n- `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`.\n- `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`.\n- `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`.\n- `maxConcurrentResolutionsPerCore` (optional, cluster parameter): The maximum number of concurrent async outbound resolutions that should be done per CPU core.  Initial value is `100`.\n- `webServiceLocalAddresses` (optional): Local addresses are the network interface IP addresses you want the web service to listen for requests. \n- `webServiceHttpPort` (optional): Specify the TCP port number for the web console and this API web service. Initial value is `5380`.\n- `webServiceEnableTls` (optional): Set this to `true` to start the HTTPS service to access web service.\n- `webServiceEnableHttp3` (optional): Set this to `true` to enable HTTP/3 protocol for the web service.\n- `webServiceHttpToTlsRedirect` (optional): Set this option to `true` to enable HTTP to HTTPS Redirection.\n- `webServiceTlsPort` (optional): Specified the TCP port number for the web console for HTTPS access.\n- `webServiceUseSelfSignedTlsCertificate` (optional): Set `true` for the web service to use an automatically generated self signed certificate when TLS certificate path is not specified.\n- `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.\n- `webServiceTlsCertificatePassword` (optional): Enter the certificate (.pfx) password, if any.\n- `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.\n- `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.\n- `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.\n- `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.\n- `enableDnsOverTls` (optional): Enable this option to accept DNS-over-TLS requests.\n- `enableDnsOverHttps` (optional): Enable this option to accept DNS-over-HTTPS requests.\n- `enableDnsOverQuic` (optional): Enable this option to accept DNS-over-QUIC requests.\n- `dnsOverUdpProxyPort` (optional): The UDP port number for DNS-over-UDP-PROXY protocol. Initial value is `538`.\n- `dnsOverTcpProxyPort` (optional): The TCP port number for DNS-over-TCP-PROXY protocol. Initial value is `538`.\n- `dnsOverHttpPort` (optional): The TCP port number for DNS-over-HTTP protocol. Initial value is `80`.\n- `dnsOverTlsPort` (optional): The TCP port number for DNS-over-TLS protocol. Initial value is `853`.\n- `dnsOverHttpsPort` (optional): The TCP port number for DNS-over-HTTPS protocol. Initial value is `443`.\n- `dnsOverQuicPort` (optional): The UDP port number for DNS-over-QUIC protocol. Initial value is `853`.\n- `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.\n- `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.\n- `dnsTlsCertificatePassword` (optional): Enter the certificate (.pfx) password, if any.\n- `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.\n- `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`].\n- `recursion` (optional, cluster parameter): Sets the recursion policy for the DNS server. Valid values are [`Deny`, `Allow`, `AllowOnlyForPrivateNetworks`, `UseSpecifiedNetworkACL`].\n- `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`.\n- `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`.\n- `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`.\n- `resolverRetries` (optional, cluster parameter): The number of retries that the recursive resolver must do.\n- `resolverTimeout` (optional, cluster parameter): The timeout value in milliseconds for the recursive resolver.\n- `resolverConcurrency` (optional, cluster parameter): The number of concurrent requests that should be sent by the recursive resolver to the name servers.\n- `resolverMaxStackCount` (optional, cluster parameter): The max stack count that the recursive resolver must use.\n- `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.\n- `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`.\n- `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`.\n- `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.\n- `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.\n- `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.\n- `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`.\n- `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`.\n- `cacheNegativeRecordTtl` (optional): The negative TTL value to use when there is no SOA MINIMUM value available. Initial value is `300`.\n- `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`.\n- `cachePrefetchEligibility` (optional): The minimum initial TTL value of a record needed to be eligible for prefetching.\n- `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.\n- `cachePrefetchSampleIntervalInMinutes` (optional): The interval to sample eligible domain names from last hour stats for auto prefetch.\n- `cachePrefetchSampleEligibilityHitsPerHour` (optional): Minimum required hits per hour for a domain name to be eligible for auto prefetch.\n- `enableBlocking` (optional, cluster parameter): Sets the DNS server to block domain names using Blocked Zone and Block List Zone.\n- `allowTxtBlockingReport` (optional, cluster parameter): Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests.\n- `blockingBypassList` (optional, cluster parameter): A comma separated list of IP addresses or network addresses that are allowed to bypass blocking.\n- `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.\n- `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.\n- `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`.\n- `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.\n- `blockListUpdateIntervalHours` (optional, cluster parameter): The interval in hours to automatically download and update the block lists. Initial value is `24`.\n- `proxyType` (optional, cluster parameter): The type of proxy protocol to be used. Valid values are [`None`, `Http`, `Socks5`].\n- `proxyAddress` (optional, cluster parameter): The proxy server hostname or IP address.\n- `proxyPort` (optional, cluster parameter): The proxy server port.\n- `proxyUsername` (optional, cluster parameter): The proxy server username.\n- `proxyPassword` (optional, cluster parameter): The proxy server password.\n- `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.\n- `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.\n- `forwarderProtocol` (optional, cluster parameter): The forwarder DNS transport protocol to be used. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`].\n- `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.\n- `forwarderRetries` (optional, cluster parameter): The number of retries that the forwarder DNS client must do.\n- `forwarderTimeout` (optional, cluster parameter): The timeout value in milliseconds for the forwarder DNS client.\n- `forwarderConcurrency` (optional, cluster parameter): The number of concurrent requests that the forwarder DNS client should do.\n- `loggingType` (optional): Specifies how the error logs and audit logs are written. The valid values are [`None`, `File`, `Console`, `FileAndConsole`]. Initial value is `File`.\n- `enableLogging` (optional)(obsolete, use `loggingType` instead): Enable this option to log error and audit logs into the log file. Initial value is `true`.\n- `ignoreResolverLogs` (optional): Enable this option to stop logging domain name resolution errors into the log file.\n- `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`.\n- `useLocalTime` (optional): Enable this option to use local time instead of UTC for logging.  Initial value is `false`.\n- `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`.\n- `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.\n- `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.\n- `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.\n\nNOTE! The parameters marked as a \"cluster parameter\" are synced automatically across all the Cluster nodes when Clustering is initialized.\n\nREQUEST: 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.\n\nRESPONSE:\nThis call returns the newly updated settings in the same format as that of the `getDnsSettings` call.\n\n### Get TSIG Key Names\n\nReturns a list of TSIG key names that are configured in the DNS server Settings.\n\nURL:\\\n`http://localhost:5380/api/settings/getTsigKeyNames?token=x`\n\nPERMISSIONS:\\\nSettings: View OR Zones: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"tsigKeyNames\": [\n\t\t\t\"key1\",\n\t\t\t\"key2\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Force Update Block Lists\n\nThis call allows to reset the next update schedule and force download and update of the block lists.\n\nURL:\\\n`http://localhost:5380/api/settings/forceUpdateBlockLists?token=x`\n\nOBSOLETE PATH:\\\n`/api/forceUpdateBlockLists`\n\nPERMISSIONS:\\\nSettings: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\nNOTE! This API call is synced automatically across all Cluster nodes when Clustering is initialized.\n\n### Temporarily Disable Block Lists\n\nThis call temporarily disables the block lists and block list zones.\n\nURL:\\\n`http://localhost:5380/api/settings/temporaryDisableBlocking?token=x&minutes=5`\n\nOBSOLETE PATH:\\\n`/api/temporaryDisableBlocking`\n\nPERMISSIONS:\\\nSettings: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `minutes`: The time in minutes to disable the blocklist for.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\",\n\t\"response\": {\n\t\t\"temporaryDisableBlockingTill\": \"2021-10-10T01:14:27.1106773Z\"\n\t}\n}\n```\n\nNOTE! This API call is synced automatically across all Cluster nodes when Clustering is initialized.\n\n### Backup Settings\n\nThis call returns a zip file containing copies of all the items that were requested to be backed up.\n\nURL:\\\n`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`\n\nOBSOLETE PATH:\\\n`/api/backupSettings`\n\nPERMISSIONS:\\\nSettings: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `authConfig` (optional): Set to `true` to backup the authentication config file. Default value is `false`.\n- `clusterConfig` (optional): Set to `true` to backup the Cluster config file. Default value is `false`.\n- `webServiceSettings` (optional): Set to `true` to backup the web service config file. Default value is `false`.\n- `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`.\n- `logSettings` (optional): Set to `true` to backup log settings file. Default value is `false`.\n- `zones` (optional): Set to `true` to backup DNS zone files. Default value is `false`.\n- `allowedZones` (optional): Set to `true` to backup allowed zones file. Default value is `false`.\n- `blockedZones` (optional): Set to `true` to backup blocked zones file. Default value is `false`.\n- `blockLists` (optional): Set to `true` to backup block lists cache files. Default value is `false`.\n- `apps` (optional): Set to `true` to backup the installed DNS apps. Default value is `false`.\n- `scopes` (optional): Set to `true` to backup DHCP scope files. Default value is `false`.\n- `stats` (optional): Set to `true` to backup dashboard stats files. Default value is `false`.\n- `logs` (optional): Set to `true` to backup log files. Default value is `false`.\n\nRESPONSE:\nA zip file with content type `application/zip` and content disposition set to `attachment`.\n\n### Restore Settings\n\nThis call restores selected items from a given backup zip file.\n\nURL:\\\n`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`\n\nOBSOLETE PATH:\\\n`/api/restoreSettings`\n\nPERMISSIONS:\\\nSettings: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `authConfig` (optional): Set to `true` to restore the authentication config file. Default value is `false`.\n- `clusterConfig` (optional): Set to `true` to restore the Cluster config file. Default value is `false`.\n- `webServiceSettings` (optional): Set to `true` to restore the web service config file. Default value is `false`.\n- `dnsSettings` (optional): Set to `true` to restore DNS settings and certificate files. Default value is `false`.\n- `logSettings` (optional): Set to `true` to restore log settings file. Default value is `false`.\n- `zones` (optional): Set to `true` to restore DNS zone files. Default value is `false`.\n- `allowedZones` (optional): Set to `true` to restore allowed zones file. Default value is `false`.\n- `blockedZones` (optional): Set to `true` to restore blocked zones file. Default value is `false`.\n- `blockLists` (optional): Set to `true` to restore block lists cache files. Default value is `false`.\n- `apps` (optional): Set to `true` to restore the DNS apps. Default value is `false`.\n- `scopes` (optional): Set to `true` to restore DHCP scope files. Default value is `false`.\n- `stats` (optional): Set to `true` to restore dashboard stats files. Default value is `false`.\n- `logs` (optional): Set to `true` to restore log files. Default value is `false`.\n- `deleteExistingFiles` (optional). Set to `true` to delete existing files for selected items. Default value is `false`.\n\nREQUEST:\nThis is a `POST` request call where the request must be multi-part form data with the backup zip file data in binary format.\n\nRESPONSE:\nThis call returns the newly updated settings in the same format as that of the `getDnsSettings` call.\n\n## DHCP API Calls\n\nAllows managing the built-in DHCP server.\n\n### List DHCP Leases\n\nLists all the DHCP leases.\n\nURL:\\\n`http://localhost:5380/api/dhcp/leases/list?token=x`\n\nOBSOLETE PATH:\\\n`/api/listDhcpLeases`\n\nPERMISSIONS:\\\nDhcpServer: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"leases\": [\n\t\t\t{\n\t\t\t\t\"scope\": \"Default\",\n\t\t\t\t\"type\": \"Reserved\",\n\t\t\t\t\"hardwareAddress\": \"00-00-00-00-00-00\",\n\t\t\t\t\"clientIdentifier\": \"1-000000000000\",\n\t\t\t\t\"address\": \"192.168.1.5\",\n\t\t\t\t\"hostName\": \"server1.local\",\n\t\t\t\t\"leaseObtained\": \"08/25/2020 17:52:51\",\n\t\t\t\t\"leaseExpires\": \"09/26/2020 14:27:12\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"scope\": \"Default\",\n\t\t\t\t\"type\": \"Dynamic\",\n\t\t\t\t\"hardwareAddress\": \"00-00-00-00-00-00\",\n\t\t\t\t\"clientIdentifier\": \"1-000000000000\",\n\t\t\t\t\"address\": \"192.168.1.13\",\n\t\t\t\t\"hostName\": null,\n\t\t\t\t\"leaseObtained\": \"06/15/2020 16:41:46\",\n\t\t\t\t\"leaseExpires\": \"09/25/2020 12:39:54\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"scope\": \"Default\",\n\t\t\t\t\"type\": \"Dynamic\",\n\t\t\t\t\"hardwareAddress\": \"00-00-00-00-00-00\",\n\t\t\t\t\"clientIdentifier\": \"1-000000000000\",\n\t\t\t\t\"address\": \"192.168.1.15\",\n\t\t\t\t\"hostName\": \"desktop-ea2miaf.local\",\n\t\t\t\t\"leaseObtained\": \"06/18/2020 12:19:03\",\n\t\t\t\t\"leaseExpires\": \"09/25/2020 12:17:11\"\n\t\t\t},\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Remove DHCP Lease\n\nRemoves 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.\n\nURL:\\\n`http://localhost:5380/api/dhcp/leases/remove?token=x&name=Default&hardwareAddress=00:00:00:00:00:00`\n\nOBSOLETE PATH:\\\n`/api/removeDhcpLease`\n\nPERMISSIONS:\\\nDhcpServer: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n- `clientIdentifier` (optional): The client identifier for the lease. Either `hardwareAddress` or `clientIdentifier` must be specified.\n- `hardwareAddress` (optional): The MAC address of the device bearing the dynamic/reserved lease. Either `hardwareAddress` or `clientIdentifier` must be specified.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Convert To Reserved Lease\n\nConverts a dynamic lease to reserved lease.\n\nURL:\\\n`http://localhost:5380/api/dhcp/leases/convertToReserved?token=x&name=Default&hardwareAddress=00:00:00:00:00:00`\n\nOBSOLETE PATH:\\\n`/api/convertToReservedLease`\n\nPERMISSIONS:\\\nDhcpServer: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n- `clientIdentifier` (optional): The client identifier for the lease. Either `hardwareAddress` or `clientIdentifier` must be specified.\n- `hardwareAddress` (optional): The MAC address of the device bearing the dynamic lease. Either `hardwareAddress` or `clientIdentifier` must be specified.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Convert To Dynamic Lease\n\nConverts a reserved lease to dynamic lease.\n\nURL:\\\n`http://localhost:5380/api/dhcp/leases/convertToDynamic?token=x&name=Default&hardwareAddress=00:00:00:00:00:00`\n\nOBSOLETE PATH:\\\n`/api/convertToDynamicLease`\n\nPERMISSIONS:\\\nDhcpServer: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n- `clientIdentifier` (optional): The client identifier for the lease. Either `hardwareAddress` or `clientIdentifier` must be specified.\n- `hardwareAddress` (optional): The MAC address of the device bearing the reserved lease. Either `hardwareAddress` or `clientIdentifier` must be specified.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### List DHCP Scopes\n\nLists all the DHCP scopes available on the server.\n\nURL:\\\n`http://localhost:5380/api/dhcp/scopes/list?token=x`\n\nOBSOLETE PATH:\\\n`/api/listDhcpScopes`\n\nPERMISSIONS:\\\nDhcpServer: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"scopes\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Default\",\n\t\t\t\t\"enabled\": false,\n\t\t\t\t\"startingAddress\": \"192.168.1.1\",\n\t\t\t\t\"endingAddress\": \"192.168.1.254\",\n\t\t\t\t\"subnetMask\": \"255.255.255.0\",\n\t\t\t\t\"networkAddress\": \"192.168.1.0\",\n\t\t\t\t\"broadcastAddress\": \"192.168.1.255\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get DHCP Scope\n\nGets the complete details of the scope configuration.\n\nURL:\\\n`http://localhost:5380/api/dhcp/scopes/get?token=x&name=Default`\n\nOBSOLETE PATH:\\\n`/api/getDhcpScope`\n\nPERMISSIONS:\\\nDhcpServer: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"name\": \"Default\",\n\t\t\"startingAddress\": \"192.168.1.1\",\n\t\t\"endingAddress\": \"192.168.1.254\",\n\t\t\"subnetMask\": \"255.255.255.0\",\n\t\t\"leaseTimeDays\": 7,\n\t\t\"leaseTimeHours\": 0,\n\t\t\"leaseTimeMinutes\": 0,\n\t\t\"offerDelayTime\": 0,\n\t\t\"pingCheckEnabled\": false,\n\t\t\"pingCheckTimeout\": 1000,\n\t\t\"pingCheckRetries\": 2,\n\t\t\"domainName\": \"local\",\n\t\t\"domainSearchList\": [\n\t\t\t\"home.arpa\",\n\t\t\t\"lan\"\n\t\t],\n\t\t\"dnsUpdates\": true,\n\t\t\"dnsOverwriteForDynamicLease\": false,\n\t\t\"dnsTtl\": 900,\n\t\t\"serverAddress\": \"192.168.1.1\",\n\t\t\"serverHostName\": \"tftp-server-1\",\n\t\t\"bootFileName\": \"boot.bin\",\n\t\t\"routerAddress\": \"192.168.1.1\",\n\t\t\"useThisDnsServer\": false,\n\t\t\"dnsServers\": [\n\t\t\t\"192.168.1.5\"\n\t\t],\n\t\t\"winsServers\": [\n\t\t\t\"192.168.1.5\"\n\t\t],\n\t\t\"ntpServers\": [\n\t\t\t\"192.168.1.5\"\n\t\t],\n\t\t\"staticRoutes\": [\n\t\t\t{\n\t\t\t\t\"destination\": \"172.16.0.0\",\n\t\t\t\t\"subnetMask\": \"255.255.255.0\",\n\t\t\t\t\"router\": \"192.168.1.2\"\n\t\t\t}\n\t\t],\n\t\t\"vendorInfo\": [\n\t\t\t{\n\t\t\t\t\"identifier\": \"substring(vendor-class-identifier,0,9)==\\\"PXEClient\\\"\",\n\t\t\t\t\"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\"\n\t\t\t}\n\t\t],\n\t\t\"capwapAcIpAddresses\": [\n\t\t\t\"192.168.1.2\"\n\t\t],\n\t\t\"tftpServerAddresses\": [\n\t\t\t\"192.168.1.5\",\n\t\t\t\"192.168.1.6\"\n\t\t],\n\t\t\"genericOptions\": [\n\t\t\t{\n\t\t\t\t\"code\": 150,\n\t\t\t\t\"value\": \"C0:A8:01:01\"\n\t\t\t}\n\t\t],\n\t\t\"exclusions\": [\n\t\t\t{\n\t\t\t\t\"startingAddress\": \"192.168.1.1\",\n\t\t\t\t\"endingAddress\": \"192.168.1.10\"\n\t\t\t}\n\t\t],\n\t\t\"reservedLeases\": [\n\t\t\t{\n\t\t\t\t\"hostName\": null,\n\t\t\t\t\"hardwareAddress\": \"00-00-00-00-00-00\",\n\t\t\t\t\"address\": \"192.168.1.10\",\n\t\t\t\t\"comments\": \"comments\"\n\t\t\t}\n\t\t],\n\t\t\"allowOnlyReservedLeases\": false,\n\t\t\"blockLocallyAdministeredMacAddresses\": true,\n\t\t\"ignoreClientIdentifierOption\": true\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set DHCP Scope\n\nSets the DHCP scope configuration.\n\nURL:\\\n`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`\n\nOBSOLETE PATH:\\\n`/api/setDhcpScope`\n\nPERMISSIONS:\\\nDhcpServer: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n- `newName` (optional): The new name of the DHCP scope to rename an existing scope.\n- `startingAddress` (optional): The starting IP address of the DHCP scope. This parameter is required when creating a new scope.\n- `endingAddress` (optional): The ending IP address of the DHCP scope. This parameter is required when creating a new scope.\n- `subnetMask` (optional): The subnet mask of the network. This parameter is required when creating a new scope.\n- `leaseTimeDays` (optional): The lease time in number of days.\n- `leaseTimeHours` (optional): The lease time in number of hours.\n- `leaseTimeMinutes` (optional): The lease time in number of minutes.\n- `offerDelayTime` (optional): The time duration in milliseconds that the DHCP server delays sending an DHCPOFFER message.\n- `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.\n- `pingCheckTimeout` (optional): The timeout interval to wait for an ping reply.\n- `pingCheckRetries` (optional): The maximum number of ping requests to try.\n- `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)\n- `domainSearchList` (optional): A comma separated list of domain names that the clients can use as a suffix when searching a domain name. (Option 119)\n- `dnsUpdates` (optional): Set this option to `true` to allow the DHCP server to automatically update forward and reverse DNS entries for clients.\n- `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.\n- `dnsTtl` (optional): The TTL value used for forward and reverse DNS records.\n- `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)\n- `serverHostName` (optional): The optional bootstrap server host name to be used by the clients to identify the TFTP server. (sname/Option 66)\n- `bootFileName` (optional): The boot file name stored on the bootstrap TFTP server to be used by the clients. (file/Option 67)\n- `routerAddress` (optional): The default gateway IP address to be used by the clients. (Option 3)\n- `useThisDnsServer` (optional): Tells the DHCP server to use this DNS server's IP address to configure the DNS Servers DHCP option for clients.\n- `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)\n- `winsServers` (optional): A comma separated list of NBNS/WINS server IP addresses to be used by the clients. (Option 44)\n- `ntpServers` (optional): A comma separated list of Network Time Protocol (NTP) server IP addresses to be used by the clients. (Option 42)\n- `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)\n- `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)\n- `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.\n- `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)\n- `tftpServerAddresses` (optional): A comma separated list of TFTP Server Address or the VoIP Configuration Server Address. (Option 150)\n- `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}`.\n- `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.\n- `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.\n- `allowOnlyReservedLeases` (optional): Set this parameter to `true` to stop dynamic IP address allocation and allocate only reserved IP addresses.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Add Reserved Lease\n\nAdds a reserved lease entry to the specified scope.\n\nURL:\\\n`http://localhost:5380/api/dhcp/scopes/addReservedLease?token=x&name=Default&hardwareAddress=00:00:00:00:00:00&ipAddress=192.168.1.11`\n\nPERMISSIONS:\\\nDhcpServer: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n- `hardwareAddress`: The MAC address of the client.\n- `ipAddress`: The reserved IP address for the client.\n- `hostName` (optional): The hostname of the client to override.\n- `comments` (optional): Comments for the reserved lease entry.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Remove Reserved Lease\n\nRemoved a reserved lease entry from the specified scope.\n\nURL:\\\n`http://localhost:5380/api/dhcp/scopes/removeReservedLease?token=x&name=Default&hardwareAddress=00:00:00:00:00:00`\n\nPERMISSIONS:\\\nDhcpServer: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n- `hardwareAddress`: The MAC address of the client.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Enable DHCP Scope\n\nEnables the DHCP scope allowing the server to allocate leases.\n\nURL:\\\n`http://localhost:5380/api/dhcp/scopes/enable?token=x&name=Default`\n\nOBSOLETE PATH:\\\n`/api/enableDhcpScope`\n\nPERMISSIONS:\\\nDhcpServer: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Disable DHCP Scope\n\nDisables the DHCP scope and stops any further lease allocations.\n\nURL:\\\n`http://localhost:5380/api/dhcp/scopes/disable?token=x&name=Default`\n\nOBSOLETE PATH:\\\n`/api/disableDhcpScope`\n\nPERMISSIONS:\\\nDhcpServer: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete DHCP Scope\n\nPermanently deletes the DHCP scope from the disk.\n\nURL:\\\n`http://localhost:5380/api/dhcp/scopes/delete?token=x&name=Default`\n\nOBSOLETE PATH:\\\n`/api/deleteDhcpScope`\n\nPERMISSIONS:\\\nDhcpServer: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `name`: The name of the DHCP scope.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n## Administration API Calls\n\nAllows managing the DNS server administration which includes managing all sessions, users, groups, and permissions.\n\n### List Sessions\n\nReturns a list of active user sessions.\n\nURL:\\\n`http://localhost:5380/api/admin/sessions/list?token=x`\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"sessions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": true,\n\t\t\t\t\"partialToken\": \"272f4890427b9ab5\",\n\t\t\t\t\"type\": \"Standard\",\n\t\t\t\t\"tokenName\": null,\n\t\t\t\t\"lastSeen\": \"2022-09-17T13:23:44.9972772Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": false,\n\t\t\t\t\"partialToken\": \"ddfaecb8e9325e77\",\n\t\t\t\t\"type\": \"ApiToken\",\n\t\t\t\t\"tokenName\": \"MyToken1\",\n\t\t\t\t\"lastSeen\": \"2022-09-17T13:22:45.6710766Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Create API Token\n\nAllows 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.\n\nURL:\\\n`http://localhost:5380/api/admin/sessions/createToken?token=x&user=admin&tokenName=MyToken1`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `user`: The username for the user account for which to generate the API token.\n- `tokenName`: The name of the created token to identify its session.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"username\": \"admin\",\n\t\t\"tokenName\": \"MyToken1\",\n\t\t\"token\": \"ddfaecb8e9325e77865ee7e100f89596a65d3eae0e6dddcb33172355b95a64af\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Session\n\nDeletes a specified user's session.\n\nURL:\\\n`http://localhost:5380/api/admin/sessions/delete?token=x&partialToken=ddfaecb8e9325e77`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `partialToken`: The partial token of the session to delete that was returned by the list of sessions.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### List Users\n\nReturns a list of all users.\n\nURL:\\\n`http://localhost:5380/api/admin/users/list?token=x`\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"users\": [\n\t\t\t{\n\t\t\t\t\"displayName\": \"Administrator\",\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"previousSessionLoggedOn\": \"2022-09-17T13:20:32.7933783Z\",\n\t\t\t\t\"previousSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"recentSessionLoggedOn\": \"2022-09-17T13:22:45.671081Z\",\n\t\t\t\t\"recentSessionRemoteAddress\": \"127.0.0.1\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"displayName\": \"Shreyas Zare\",\n\t\t\t\t\"username\": \"shreyas\",\n\t\t\t\t\"disabled\": false,\n\t\t\t\t\"previousSessionLoggedOn\": \"0001-01-01T00:00:00Z\",\n\t\t\t\t\"previousSessionRemoteAddress\": \"0.0.0.0\",\n\t\t\t\t\"recentSessionLoggedOn\": \"0001-01-01T00:00:00Z\",\n\t\t\t\t\"recentSessionRemoteAddress\": \"0.0.0.0\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Create User\n\nCreates a new user account.\n\nURL:\\\n`http://localhost:5380/api/admin/users/create?token=x&displayName=User&user=user1&pass=password`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `user`: A unique username for the user account.\n- `pass`: A password for the user account.\n- `displayName` (optional): The display name for the user account.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"displayName\": \"User\",\n\t\t\"username\": \"user1\",\n\t\t\"disabled\": false,\n\t\t\"previousSessionLoggedOn\": \"0001-01-01T00:00:00\",\n\t\t\"previousSessionRemoteAddress\": \"0.0.0.0\",\n\t\t\"recentSessionLoggedOn\": \"0001-01-01T00:00:00\",\n\t\t\"recentSessionRemoteAddress\": \"0.0.0.0\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get User Details\n\nReturns a user account profile details.\n\nURL:\\\n`http://localhost:5380/api/admin/users/get?token=x&user=admin&includeGroups=true\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `user`: The username for the user account.\n- `includeGroups` (optional): Set `true` to include a list of groups in response.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"displayName\": \"Administrator\",\n\t\t\"username\": \"admin\",\n\t\t\"totpEnabled\": false,\n\t\t\"disabled\": false,\n\t\t\"previousSessionLoggedOn\": \"2022-09-16T13:22:45.671Z\",\n\t\t\"previousSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"recentSessionLoggedOn\": \"2022-09-18T09:55:26.9800695Z\",\n\t\t\"recentSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"sessionTimeoutSeconds\": 1800,\n\t\t\"memberOfGroups\": [\n\t\t\t\"Administrators\"\n\t\t],\n\t\t\"sessions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": false,\n\t\t\t\t\"partialToken\": \"1f8011516cea27af\",\n\t\t\t\t\"type\": \"Standard\",\n\t\t\t\t\"tokenName\": null,\n\t\t\t\t\"lastSeen\": \"2022-09-18T09:55:40.6519988Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": false,\n\t\t\t\t\"partialToken\": \"ddfaecb8e9325e77\",\n\t\t\t\t\"type\": \"ApiToken\",\n\t\t\t\t\"tokenName\": \"MyToken1\",\n\t\t\t\t\"lastSeen\": \"2022-09-17T13:22:45.671Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t}\n\t\t],\n\t\t\"groups\": [\n\t\t\t\"Administrators\",\n\t\t\t\"DHCP Administrators\",\n\t\t\t\"DNS Administrators\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set User Details\n\nAllows changing user account profile details.\n\nURL:\\\n`http://localhost:5380/api/admin/users/set?token=x&user=admin&displayName=Administrator&disabled=false&sessionTimeoutSeconds=1800&memberOfGroups=Administrators`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `user`: The username for the user account.\n- `displayName` (optional): The display name for the user account.\n- `newUser` (optional): A new username for renaming the username for the user account.\n- `totpEnabled` (optional): Set to `false` to disable 2FA for the user account. The parameter cannot have a `true` value.\n- `disabled` (optional): Set `true` to disable the user account and delete all its active sessions.\n- `sessionTimeoutSeconds` (optional): A session time out value in seconds for the user account.\n- `newPass` (optional): A new password to reset the user account password.\n- `iterations` (optional): The number of iterations for PBKDF2 SHA256 password hashing. This is only used with the `newPass` option.\n- `memberOfGroups` (optional): A list of comma separated group names that the user must be set as a member.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"displayName\": \"Administrator\",\n\t\t\"username\": \"admin\",\n\t\t\"totpEnabled\": false,\n\t\t\"disabled\": false,\n\t\t\"previousSessionLoggedOn\": \"2022-09-17T13:22:45.671Z\",\n\t\t\"previousSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"recentSessionLoggedOn\": \"2022-09-18T09:55:26.9800695Z\",\n\t\t\"recentSessionRemoteAddress\": \"127.0.0.1\",\n\t\t\"sessionTimeoutSeconds\": 1800,\n\t\t\"memberOfGroups\": [\n\t\t\t\"Administrators\"\n\t\t],\n\t\t\"sessions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": false,\n\t\t\t\t\"partialToken\": \"1f8011516cea27af\",\n\t\t\t\t\"type\": \"Standard\",\n\t\t\t\t\"tokenName\": null,\n\t\t\t\t\"lastSeen\": \"2022-09-18T09:59:19.9034491Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"isCurrentSession\": false,\n\t\t\t\t\"partialToken\": \"ddfaecb8e9325e77\",\n\t\t\t\t\"type\": \"ApiToken\",\n\t\t\t\t\"tokenName\": \"MyToken1\",\n\t\t\t\t\"lastSeen\": \"2022-09-17T13:22:45.671Z\",\n\t\t\t\t\"lastSeenRemoteAddress\": \"127.0.0.1\",\n\t\t\t\t\"lastSeenUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete User\n\nDeletes a user account.\n\nURL:\\\n`http://localhost:5380/api/admin/users/delete?token=x&user=user1`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `user`: The username for the user account to delete.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### List Groups\n\nReturns a list of all groups.\n\nURL:\\\n`http://localhost:5380/api/admin/groups/list?token=x`\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"groups\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\"description\": \"Super administrators\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"DHCP Administrators\",\n\t\t\t\t\"description\": \"DHCP service administrators\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\"description\": \"DNS service administrators\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Create Group\n\nCreates a new group.\n\nURL:\\\n`http://localhost:5380/api/admin/groups/create?token=x&group=Group1&description=My%20description`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `group`: The name of the group to create.\n- `description` (optional): The description text for the group.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"name\": \"Group1\",\n\t\t\"description\": \"My description\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get Group Details\n\nReturns the details for a group.\n\nURL:\\\n`http://localhost:5380/api/admin/groups/get?token=x&group=Administrators&includeUsers=true`\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `group`: The name of the group.\n- `includeUsers` (optional): Set `true` to include a list of users in response.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"name\": \"Administrators\",\n\t\t\"description\": \"Super administrators\",\n\t\t\"members\": [\n\t\t\t\"admin\"\n\t\t],\n\t\t\"users\": [\n\t\t\t\"admin\",\n\t\t\t\"shreyas\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set Group Details\n\nAllows changing group description or rename a group.\n\nURL:\\\n`http://localhost:5380/api/admin/groups/set?token=x&group=Administrators&description=Super%20administrators&members=admin`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `group`: The name of the group to update.\n- `newGroup` (optional): A new group name to rename the group.\n- `description` (optional): A new group description.\n- `members` (optional): A comma separated list of usernames to set as the group's members.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"name\": \"Administrators\",\n\t\t\"description\": \"Super administrators\",\n\t\t\"members\": [\n\t\t\t\"admin\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Group\n\nAllows deleting a group.\n\nURL:\\\n`http://localhost:5380/api/admin/groups/delete?token=x&group=Group1`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `group`: The name of the group to delete.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### List Permissions\n\nURL:\\\n`http://localhost:5380/api/admin/permissions/list?token=x`\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"permissions\": [\n\t\t\t{\n\t\t\t\t\"section\": \"Dashboard\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Zones\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DHCP Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Cache\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Allowed\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Blocked\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Apps\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"DnsClient\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DHCP Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Settings\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"DhcpServer\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DHCP Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Administration\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"section\": \"Logs\",\n\t\t\t\t\"userPermissions\": [],\n\t\t\t\t\"groupPermissions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": true,\n\t\t\t\t\t\t\"canDelete\": true\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DHCP Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"DNS Administrators\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\t\t\"canView\": true,\n\t\t\t\t\t\t\"canModify\": false,\n\t\t\t\t\t\t\"canDelete\": false\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get Permission Details\n\nGets details of the permissions for the specified section.\n\nURL:\\\n`http://localhost:5380/api/admin/permissions/get?token=x&section=Dashboard&includeUsersAndGroups=true`\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `section`: The name of the section as given in the list of permissions API call.\n- `includeUsersAndGroups` (optional): Set to `true` to include a list of users and groups in the response.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"section\": \"Dashboard\",\n\t\t\"userPermissions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"shreyas\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": false,\n\t\t\t\t\"canDelete\": false\n\t\t\t}\n\t\t],\n\t\t\"groupPermissions\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": false,\n\t\t\t\t\"canDelete\": false\n\t\t\t}\n\t\t],\n\t\t\"users\": [\n\t\t\t\"admin\",\n\t\t\t\"shreyas\"\n\t\t],\n\t\t\"groups\": [\n\t\t\t\"Administrators\",\n\t\t\t\"DHCP Administrators\",\n\t\t\t\"DNS Administrators\",\n\t\t\t\"Everyone\"\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Set Permission Details\n\nAllows changing permissions for the specified section.\n\nURL:\\\n`http://localhost:5380/api/admin/permissions/set?token=x&section=Dashboard&userPermissions=shreyas|true|false|false&groupPermissions=Administrators|true|true|true|Everyone|true|false|false`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `section`: The name of the section as given in the list of permissions API call.\n- `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\n- `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\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"section\": \"Dashboard\",\n\t\t\"userPermissions\": [\n\t\t\t{\n\t\t\t\t\"username\": \"shreyas\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": false,\n\t\t\t\t\"canDelete\": false\n\t\t\t}\n\t\t],\n\t\t\"groupPermissions\": [\n\t\t\t{\n\t\t\t\t\"name\": \"Administrators\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": true,\n\t\t\t\t\"canDelete\": true\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"name\": \"Everyone\",\n\t\t\t\t\"canView\": true,\n\t\t\t\t\"canModify\": false,\n\t\t\t\t\"canDelete\": false\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Get Cluster state\n\nReturns data on the current state of the Cluster.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/state?token=x&includeServerIpAddresses=true`\n\nPERMISSIONS:\\\nAdministration: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-26T12:30:16Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1342079372,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddress\": \"192.168.10.5\",\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"id\": 1653399468,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddress\": \"192.168.10.101\",\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Unreachable\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"id\": 1843286864,\n\t\t\t\t\"name\": \"server3.example.com\",\n\t\t\t\t\"url\": \"https://server3.example.com:53443/\",\n\t\t\t\t\"ipAddress\": \"192.168.10.102\",\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Connected\",\n\t\t\t\t\"lastSeen\": \"2025-09-26T12:30:16Z\"\n\t\t\t}\n\t\t],\n\t\t\"serverIpAddresses\": [\n\t\t\t\"192.168.10.5\",\n\t\t\t\"192.168.120.1\",\n\t\t\t\"192.168.180.1\",\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Initialize Cluster\n\n 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. \n\nNote! 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.\n\nNote! 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.\n\nWarning! The Cluster domain name cannot be changed later. Make sure that you enter the correct domain name before proceeding. \n\nURL:\\\n`http://localhost:5380/api/admin/cluster/init?token=x&clusterDomain=example.com&primaryNodeIpAddresses=192.168.10.5`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `clusterDomain`: The fully qualified domain name to be used to identify the new Cluster.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1081800048,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Cluster\n\nThe 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.\n \nNote! 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.  \n\nURL:\\\n`http://localhost:5380/api/admin/cluster/primary/delete?token=x&forceDelete=false`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": false,\n\t\t\"dnsServerDomain\": \"server1\",\n\t\t\"version\": \"14.0\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Join Cluster\n\nThis 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/primary/join?token=x`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `secondaryNodeId`: The Secondary node ID that was generated randomly when the server is initializing as a Secondary node.\n- `secondaryNodeUrl`: The HTTPS URL of the Secondary node's API web service.\n- `secondaryNodeIpAddresses`: A comma separated list of Secondary node IP addresses.\n- `secondaryNodeCertificate`: The Secondary node's TLS certificate used by the API web service.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-26T12:30:16Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1342079372,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"id\": 1653399468,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.101\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Unreachable\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Remove Secondary Node\n\nThe 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/primary/removeSecondary?token=X&secondaryNodeId=811905692`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `secondaryNodeId`: The Secondary node ID which needs to be asked to leave the Cluster.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1151850285,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete Secondary Node\n\nThe 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/primary/deleteSecondary?token=X&secondaryNodeId=811905692`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `secondaryNodeId`: The Secondary node ID which must be deleted from the Cluster immediately.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1151850285,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Update Secondary Node\n\nThe 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.\n\nURL:\\\n`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=<base64url>`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `secondaryNodeId`: The Secondary node ID that identifies the node.\n- `secondaryNodeUrl`: The HTTPS API web service URL of the Secondary node to be updated.\n- `secondaryNodeIpAddresses`: A comma separated list of IP addresses of the Secondary node to be updated.\n- `secondaryNodeCertificate`: The web service TLS certificate encoded in Base64 URL format.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-26T12:30:16Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1342079372,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"id\": 1653399468,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.101\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Connected\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Transfer Config\n\nThe 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/primary/transferConfig?token=x&includeZones=`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `includeZones` (optional): A list of comma separated domain names of the zones that should be included to transfer DNSSEC private keys.\n\nRESPONSE HEADERS:\\\n- `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`.\n\nRESPONSE: A zip file with content type `application/zip` and content disposition set to `attachment`.\n\n### Set Cluster Options\n\nAllows setting Cluster options to be used by all Secondary nodes. This call can be made only at the Primary node.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/primary/setOptions?token=x`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `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`.\n- `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`.\n- `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`.\n- `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`.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server1.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-26T12:30:16Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1342079372,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"id\": 1653399468,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.101\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Connected\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Initialize And Join Cluster\n\nJoining 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.\n \nNote! 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.\n\nNote! 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.\n\nWarning! Joining a Cluster will cause configuration on this DNS server to be overwritten permanently for Allowed, Blocked, Apps, Settings and Administration sections! \n\nURL:\\\n`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=`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `primaryNodeUrl`: The web service HTTPS URL of the Primary node in the Cluster.\n- `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.\n- `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. \n- `primaryNodeUsername`: The username of an administrator on the Primary node in the Cluster.\n- `primaryNodePassword`: The password of the administrator user specified above.\n- `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.\n\nREQUEST:\nThis is a `POST` request call where the content type of the request must be `application/x-www-form-urlencoded`.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server2.example.com\",\n\t\t\"version\": \"14.3\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-27T13:19:55Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1151850285,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Connected\",\n\t\t\t\t\"lastSeen\": \"2025-09-27T13:19:54.6215569Z\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"id\": 811905692,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.101\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Leave Cluster\n\nThe 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.\n \nNote! Use the Force Leave Cluster option only when the Primary node is unreachable/decommissioned and thus cannot leave the Cluster gracefully. \n\nURL:\\\n`http://localhost:5380/api/admin/cluster/secondary/leave?token=x`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `forceLeave` (optional): Set to `true` to make this Secondary node to leave the Cluster without informing the Primary node.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": false,\n\t\t\"dnsServerDomain\": \"server2\",\n\t\t\"version\": \"14.0\"\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Notify\n\nThe 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/secondary/notify?token=x`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `primaryNodeId`: The calling Primary node's ID to allow identification at the Secondary node.\n- `primaryNodeUrl`: The calling Primary node's API web service URL.\n- `primaryNodeIpAddresses`: A comma separated list of the calling Primary node's IP addresses.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Resync Cluster\n\nThe 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/secondary/resync?token=x`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"status\": \"ok\"\n}\n```\n\n### Update Primary Node\n\nAllows 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/secondary/updatePrimary?token=x`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `primaryNodeUrl`: The web service HTTPS URL of the Primary node in the Cluster.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server2.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-27T13:19:55Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 1151850285,\n\t\t\t\t\"name\": \"server1.example.com\",\n\t\t\t\t\"url\": \"https://server1.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.5\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Connected\",\n\t\t\t\t\"lastSeen\": \"2025-09-27T13:19:54.6215569Z\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"id\": 811905692,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.101\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Secondary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Promote To Primary\n\nThe 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.\n\nNote! 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.\n\nNote! 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.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/secondary/promote?token=x`\n\nPERMISSIONS:\\\nAdministration: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server2.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-27T13:19:55Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 811905692,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.101\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Update Node IP Addresses\n\nAllows to update the current Cluster node's IP address. This call can be made at both the Primary and Secondary nodes.\n\nURL:\\\n`http://localhost:5380/api/admin/cluster/updateIpAddresses?token=x`\n\nPERMISSIONS:\\\nAdministration: Modify\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `ipAddresses`: A comma separated list of IP addresses to be updated for the current node.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"clusterInitialized\": true,\n\t\t\"dnsServerDomain\": \"server2.example.com\",\n\t\t\"version\": \"14.0\",\n\t\t\"clusterDomain\": \"example.com\",\n\t\t\"heartbeatRefreshIntervalSeconds\": 30,\n\t\t\"heartbeatRetryIntervalSeconds\": 10,\n\t\t\"configRefreshIntervalSeconds\": 900,\n\t\t\"configRetryIntervalSeconds\": 60,\n\t\t\"configLastSynced\": \"2025-09-27T13:19:55Z\",\n\t\t\"nodes\": [\n\t\t\t{\n\t\t\t\t\"id\": 811905692,\n\t\t\t\t\"name\": \"server2.example.com\",\n\t\t\t\t\"url\": \"https://server2.example.com:53443/\",\n\t\t\t\t\"ipAddresses\": [\n\t\t\t\t\t\"192.168.10.101\"\n\t\t\t\t],\n\t\t\t\t\"type\": \"Primary\",\n\t\t\t\t\"state\": \"Self\",\n\t\t\t\t\"lastSeen\": \"0001-01-01T00:00:00\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n## Log API Calls\n\n### List Logs\n\nLists all logs files available on the DNS server.\n\nURL:\\\n`http://localhost:5380/api/logs/list?token=x`\n\nOBSOLETE PATH:\\\n`/api/listLogs`\n\nPERMISSIONS:\\\nLogs: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"logFiles\": [\n\t\t\t{\n\t\t\t\t\"fileName\": \"2020-09-19\",\n\t\t\t\t\"size\": \"8.14 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"fileName\": \"2020-09-15\",\n\t\t\t\t\"size\": \"5.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"fileName\": \"2020-09-12\",\n\t\t\t\t\"size\": \"18.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"fileName\": \"2020-09-11\",\n\t\t\t\t\"size\": \"1.78 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"fileName\": \"2020-09-10\",\n\t\t\t\t\"size\": \"2.03 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Download Log\n\nDownloads the log file.\n\nURL:\\\n`http://localhost:5380/api/logs/download?token=x&fileName=2020-09-10&limit=2`\n\nOBSOLETE PATH:\\\n`/log/{fileName}`\n\nPERMISSIONS:\\\nLogs: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `fileName`: The `fileName` returned by the List Logs API call.\n- `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.\n\nRESPONSE:\nResponse is a downloadable file with `Content-Type: text/plain` and `Content-Disposition: attachment;filename=name`\n\n### Delete Log\n\nPermanently deletes a log file from the disk.\n\nURL: \n`http://localhost:5380/api/logs/delete?token=x&log=2020-09-19`\n\nOBSOLETE PATH:\\\n`/api/deleteLog`\n\nPERMISSIONS:\\\nLogs: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `log`: The `fileName` returned by the List Logs API call.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Delete All Logs\n\nPermanently delete all log files from the disk.\n\nURL:\\\n`http://localhost:5380/api/logs/deleteAll?token=x`\n\nOBSOLETE PATH:\\\n`/api/deleteAllLogs`\n\nPERMISSIONS:\\\nLogs: Delete\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n\nRESPONSE:\n```\n{\n\t\"response\": {},\n\t\"status\": \"ok\"\n}\n```\n\n### Query Logs\n\nQueries for logs to a specified DNS app.\n\nURL:\\\n`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=`\n\nOBSOLETE PATH:\\\n`/api/queryLogs`\n\nPERMISSIONS:\\\nLogs: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `name`: The name of the installed DNS app.\n- `classPath`: The class path of the DNS app.\n- `pageNumber` (optional): The page number of the data set to retrieve.\n- `entriesPerPage` (optional): The number of entries per page.\n- `descendingOrder` (optional): Orders the selected data set in descending order.\n- `start` (optional): The start date time in ISO 8601 format to filter the logs.\n- `end` (optional): The end date time in ISO 8601 format to filter the logs.\n- `clientIpAddress` (optional): The client IP address to filter the logs.\n- `protocol` (optional): The DNS transport protocol to filter the logs. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`].\n- `responseType` (optional): The DNS server response type to filter the logs. Valid values are [`Authoritative`, `Recursive`, `Cached`, `Blocked`, `UpstreamBlocked`, `CacheBlocked`].\n- `rcode` (optional): The DNS response code to filter the logs.\n- `qname` (optional): The query name (QNAME) in the request question section to filter the logs.\n- `qtype` (optional): The DNS resource record type (QTYPE) in the request question section to filter the logs.\n- `qclass` (optional): The DNS class (QCLASS) in the request question section to filter the logs.\n\nRESPONSE:\n```\n{\n\t\"response\": {\n\t\t\"pageNumber\": 1,\n\t\t\"totalPages\": 2,\n\t\t\"totalEntries\": 13,\n\t\t\"entries\": [\n\t\t\t{\n\t\t\t\t\"rowNumber\": 1,\n\t\t\t\t\"timestamp\": \"2021-09-10T12:22:52Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Recursive\",\n\t\t\t\t\"responseRtt\": 33.45,\n\t\t\t\t\"rcode\": \"NoError\",\n\t\t\t\t\"qname\": \"google.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"172.217.166.46\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 2,\n\t\t\t\t\"timestamp\": \"2021-09-10T12:37:02Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Blocked\",\n\t\t\t\t\"rcode\": \"NxDomain\",\n\t\t\t\t\"qname\": \"example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 3,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:13:31Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Authoritative\",\n\t\t\t\t\"rcode\": \"ServerFailure\",\n\t\t\t\t\"qname\": \"example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 4,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:14:48Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Authoritative\",\n\t\t\t\t\"rcode\": \"ServerFailure\",\n\t\t\t\t\"qname\": \"example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 5,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:27:25Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Blocked\",\n\t\t\t\t\"rcode\": \"NxDomain\",\n\t\t\t\t\"qname\": \"example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 6,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:27:29Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Blocked\",\n\t\t\t\t\"rcode\": \"NxDomain\",\n\t\t\t\t\"qname\": \"www.example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 7,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:28:36Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Blocked\",\n\t\t\t\t\"rcode\": \"NxDomain\",\n\t\t\t\t\"qname\": \"www.example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 8,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:28:41Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Blocked\",\n\t\t\t\t\"rcode\": \"NxDomain\",\n\t\t\t\t\"qname\": \"example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 9,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:28:44Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Blocked\",\n\t\t\t\t\"rcode\": \"NxDomain\",\n\t\t\t\t\"qname\": \"sdfsdf.example.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"rowNumber\": 10,\n\t\t\t\t\"timestamp\": \"2021-09-11T09:42:02Z\",\n\t\t\t\t\"clientIpAddress\": \"127.0.0.1\",\n\t\t\t\t\"protocol\": \"Udp\",\n\t\t\t\t\"responseType\": \"Recursive\",\n\t\t\t\t\"responseRtt\": 23.63,\n\t\t\t\t\"rcode\": \"NoError\",\n\t\t\t\t\"qname\": \"technitium.com\",\n\t\t\t\t\"qtype\": \"A\",\n\t\t\t\t\"qclass\": \"IN\",\n\t\t\t\t\"answer\": \"139.59.3.235\"\n\t\t\t}\n\t\t]\n\t},\n\t\"status\": \"ok\"\n}\n```\n\n### Export Query Logs\n\nQueries for logs to a specified DNS app and exports the data as a CSV file.\n\nURL:\\\n`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=`\n\nPERMISSIONS:\\\nLogs: View\n\nWHERE:\n- `token`: The session token generated by the `login` or the `createToken` call.\n- `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.\n- `name`: The name of the installed DNS app.\n- `classPath`: The class path of the DNS app.\n- `start` (optional): The start date time in ISO 8601 format to filter the logs.\n- `end` (optional): The end date time in ISO 8601 format to filter the logs.\n- `clientIpAddress` (optional): The client IP address to filter the logs.\n- `protocol` (optional): The DNS transport protocol to filter the logs. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`].\n- `responseType` (optional): The DNS server response type to filter the logs. Valid values are [`Authoritative`, `Recursive`, `Cached`, `Blocked`, `UpstreamBlocked`, `CacheBlocked`].\n- `rcode` (optional): The DNS response code to filter the logs.\n- `qname` (optional): The query name (QNAME) in the request question section to filter the logs.\n- `qtype` (optional): The DNS resource record type (QTYPE) in the request question section to filter the logs.\n- `qclass` (optional): The DNS class (QCLASS) in the request question section to filter the logs.\n\nRESPONSE: Response is a downloadable text file with `Content-Type: text/csv` and `Content-Disposition: attachment` headers set.\n"
  },
  {
    "path": "Apps/AdvancedBlockingApp/AdvancedBlockingApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>10.0</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>AdvancedBlockingApp</AssemblyName>\n\t\t<RootNamespace>AdvancedBlocking</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t\t<Nullable>enable</Nullable>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/AdvancedBlockingApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Sockets;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Http.Client;\n\nnamespace AdvancedBlocking\n{\n    public sealed class App : IDnsApplication, IDnsRequestBlockingHandler\n    {\n        #region variables\n\n        IDnsServer? _dnsServer;\n\n        DnsSOARecordData? _soaRecord;\n        DnsNSRecordData? _nsRecord;\n\n        bool _enableBlocking;\n        uint _blockingAnswerTtl;\n        int _blockListUrlUpdateIntervalHours;\n        int _blockListUrlUpdateIntervalMinutes;\n\n        Dictionary<EndPoint, string>? _localEndPointGroupMap;\n        Dictionary<NetworkAddress, string>? _networkGroupMap;\n        Dictionary<string, Group>? _groups;\n\n        Dictionary<Uri, BlockList> _allAllowListZones = [];\n        Dictionary<Uri, BlockList> _allBlockListZones = [];\n\n        Dictionary<Uri, RegexList> _allRegexAllowListZones = [];\n        Dictionary<Uri, RegexList> _allRegexBlockListZones = [];\n\n        Dictionary<Uri, AdBlockList> _allAdBlockListZones = [];\n\n        Timer? _blockListUrlUpdateTimer;\n        DateTime _blockListUrlLastUpdatedOn;\n        const int BLOCK_LIST_UPDATE_TIMER_INTERVAL = 60000;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            if (_blockListUrlUpdateTimer is not null)\n            {\n                _blockListUrlUpdateTimer.Dispose();\n                _blockListUrlUpdateTimer = null;\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private async void BlockListUrlUpdateTimerCallbackAsync(object? state)\n        {\n            try\n            {\n                if (DateTime.UtcNow > _blockListUrlLastUpdatedOn.AddHours(_blockListUrlUpdateIntervalHours).AddMinutes(_blockListUrlUpdateIntervalMinutes))\n                {\n                    if (await UpdateAllListsAsync())\n                    {\n                        //block lists were updated\n                        //save last updated on time\n                        _blockListUrlLastUpdatedOn = DateTime.UtcNow;\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer?.WriteLog(ex);\n            }\n        }\n\n        private async Task<bool> UpdateAllListsAsync()\n        {\n            List<Task<bool>> updateTasks = new List<Task<bool>>();\n\n            foreach (KeyValuePair<Uri, BlockList> allAllowListZone in _allAllowListZones)\n                updateTasks.Add(allAllowListZone.Value.UpdateAsync());\n\n            foreach (KeyValuePair<Uri, BlockList> allBlockListZone in _allBlockListZones)\n                updateTasks.Add(allBlockListZone.Value.UpdateAsync());\n\n            foreach (KeyValuePair<Uri, RegexList> allRegexAllowListZone in _allRegexAllowListZones)\n                updateTasks.Add(allRegexAllowListZone.Value.UpdateAsync());\n\n            foreach (KeyValuePair<Uri, RegexList> allRegexBlockListZone in _allRegexBlockListZones)\n                updateTasks.Add(allRegexBlockListZone.Value.UpdateAsync());\n\n            foreach (KeyValuePair<Uri, AdBlockList> allAdBlockListZone in _allAdBlockListZones)\n                updateTasks.Add(allAdBlockListZone.Value.UpdateAsync());\n\n            await Task.WhenAll(updateTasks);\n\n            foreach (Task<bool> updateTask in updateTasks)\n            {\n                bool downloaded = await updateTask;\n                if (downloaded)\n                    return true;\n            }\n\n            return false;\n        }\n\n        private static string? GetParentZone(string domain)\n        {\n            int i = domain.IndexOf('.');\n            if (i > -1)\n                return domain.Substring(i + 1);\n\n            //dont return root zone\n            return null;\n        }\n\n        private static bool IsZoneFound(HashSet<string> domains, string domain, out string? foundZone)\n        {\n            do\n            {\n                if (domains.Contains(domain))\n                {\n                    foundZone = domain;\n                    return true;\n                }\n\n                domain = GetParentZone(domain)!;\n            }\n            while (domain is not null);\n\n            foundZone = null;\n            return false;\n        }\n\n        private static bool IsZoneFound(Dictionary<Uri, BlockList> listZones, string domain, out string? foundZone, out Uri? listUri)\n        {\n            foreach (KeyValuePair<Uri, BlockList> listZone in listZones)\n            {\n                if (listZone.Value.IsZoneFound(domain, out foundZone))\n                {\n                    listUri = listZone.Key;\n                    return true;\n                }\n            }\n\n            foundZone = null;\n            listUri = null;\n            return false;\n        }\n\n        private static bool IsZoneFound(Dictionary<Uri, ListZoneEntry<BlockList>> listZones, string domain, out string? foundZone, out UrlEntry? listUri)\n        {\n            foreach (KeyValuePair<Uri, ListZoneEntry<BlockList>> listZone in listZones)\n            {\n                if (listZone.Value.List.IsZoneFound(domain, out foundZone))\n                {\n                    listUri = listZone.Value.UrlEntry;\n                    return true;\n                }\n            }\n\n            foundZone = null;\n            listUri = null;\n            return false;\n        }\n\n        private static bool IsZoneAllowed(Dictionary<Uri, ListZoneEntry<AdBlockList>> listZones, string domain, out string? foundZone, out UrlEntry? listUri)\n        {\n            foreach (KeyValuePair<Uri, ListZoneEntry<AdBlockList>> listZone in listZones)\n            {\n                if (listZone.Value.List.IsZoneAllowed(domain, out foundZone))\n                {\n                    listUri = listZone.Value.UrlEntry;\n                    return true;\n                }\n            }\n\n            foundZone = null;\n            listUri = null;\n            return false;\n        }\n\n        private static bool IsZoneBlocked(Dictionary<Uri, ListZoneEntry<AdBlockList>> listZones, string domain, out string? foundZone, out UrlEntry? listUri)\n        {\n            foreach (KeyValuePair<Uri, ListZoneEntry<AdBlockList>> listZone in listZones)\n            {\n                if (listZone.Value.List.IsZoneBlocked(domain, out foundZone))\n                {\n                    listUri = listZone.Value.UrlEntry;\n                    return true;\n                }\n            }\n\n            foundZone = null;\n            listUri = null;\n            return false;\n        }\n\n        private static bool IsMatchFound(IReadOnlyList<Regex> regices, string domain, out string? matchingPattern)\n        {\n            foreach (Regex regex in regices)\n            {\n                if (regex.IsMatch(domain))\n                {\n                    //found pattern\n                    matchingPattern = regex.ToString();\n                    return true;\n                }\n            }\n\n            matchingPattern = null;\n            return false;\n        }\n\n        private static bool IsMatchFound(Dictionary<Uri, RegexList> regexListZones, string domain, out string? matchingPattern, out Uri? listUri)\n        {\n            foreach (KeyValuePair<Uri, RegexList> regexListZone in regexListZones)\n            {\n                if (regexListZone.Value.IsMatchFound(domain, out matchingPattern))\n                {\n                    listUri = regexListZone.Key;\n                    return true;\n                }\n            }\n\n            matchingPattern = null;\n            listUri = null;\n            return false;\n        }\n\n        private static bool IsMatchFound(Dictionary<Uri, ListZoneEntry<RegexList>> regexListZones, string domain, out string? matchingPattern, out UrlEntry? listUri)\n        {\n            foreach (KeyValuePair<Uri, ListZoneEntry<RegexList>> regexListZone in regexListZones)\n            {\n                if (regexListZone.Value.List.IsMatchFound(domain, out matchingPattern))\n                {\n                    listUri = regexListZone.Value.UrlEntry;\n                    return true;\n                }\n            }\n\n            matchingPattern = null;\n            listUri = null;\n            return false;\n        }\n\n        private string? GetGroupName(DnsDatagram request, IPEndPoint remoteEP)\n        {\n            if ((request.Metadata is not null) && (request.Metadata.NameServer is not null))\n            {\n                Uri requestLocalUriEP = request.Metadata.NameServer.DoHEndPoint;\n                if (requestLocalUriEP is not null)\n                {\n                    foreach (KeyValuePair<EndPoint, string> entry in _localEndPointGroupMap!)\n                    {\n                        if (entry.Key is DomainEndPoint ep)\n                        {\n                            if (((ep.Port == 0) || (ep.Port == requestLocalUriEP.Port)) && ep.Address.Equals(requestLocalUriEP.Host, StringComparison.OrdinalIgnoreCase))\n                                return entry.Value;\n                        }\n                    }\n                }\n\n                DomainEndPoint requestLocalDomainEP = request.Metadata.NameServer.DomainEndPoint;\n                if (requestLocalDomainEP is not null)\n                {\n                    foreach (KeyValuePair<EndPoint, string> entry in _localEndPointGroupMap!)\n                    {\n                        if (entry.Key is DomainEndPoint ep)\n                        {\n                            if (((ep.Port == 0) || (ep.Port == requestLocalDomainEP.Port)) && ep.Address.Equals(requestLocalDomainEP.Address, StringComparison.OrdinalIgnoreCase))\n                                return entry.Value;\n                        }\n                    }\n                }\n\n                IPEndPoint requestLocalEP = request.Metadata.NameServer.IPEndPoint;\n                if (requestLocalEP is not null)\n                {\n                    foreach (KeyValuePair<EndPoint, string> entry in _localEndPointGroupMap!)\n                    {\n                        if (entry.Key is IPEndPoint ep)\n                        {\n                            if (((ep.Port == 0) || (ep.Port == requestLocalEP.Port)) && ep.Address.Equals(requestLocalEP.Address))\n                                return entry.Value;\n                        }\n                    }\n                }\n            }\n\n            string? groupName = null;\n            IPAddress remoteIP = remoteEP.Address;\n            NetworkAddress? network = null;\n\n            foreach (KeyValuePair<NetworkAddress, string> entry in _networkGroupMap!)\n            {\n                if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength)))\n                {\n                    network = entry.Key;\n                    groupName = entry.Value;\n                }\n            }\n\n            return groupName;\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            Directory.CreateDirectory(Path.Combine(_dnsServer.ApplicationFolder, \"blocklists\"));\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _enableBlocking = jsonConfig.GetPropertyValue(\"enableBlocking\", true);\n            _blockingAnswerTtl = jsonConfig.GetPropertyValue(\"blockingAnswerTtl\", 30u);\n            _blockListUrlUpdateIntervalHours = jsonConfig.GetPropertyValue(\"blockListUrlUpdateIntervalHours\", 24);\n            _blockListUrlUpdateIntervalMinutes = jsonConfig.GetPropertyValue(\"blockListUrlUpdateIntervalMinutes\", 0);\n\n            _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, _blockingAnswerTtl);\n            _nsRecord = new DnsNSRecordData(_dnsServer.ServerDomain);\n\n            if (jsonConfig.TryReadObjectAsMap(\"localEndPointGroupMap\",\n                delegate (string localEP, JsonElement jsonGroup)\n                {\n                    if (!EndPointExtensions.TryParse(localEP, out EndPoint ep))\n                        throw new InvalidOperationException(\"Local end point group map contains an invalid end point: \" + localEP);\n\n                    return new Tuple<EndPoint, string>(ep, jsonGroup.GetString() ?? \"\");\n                },\n                out Dictionary<EndPoint, string> localEndPointGroupMap))\n            {\n                _localEndPointGroupMap = localEndPointGroupMap;\n            }\n\n            _networkGroupMap = jsonConfig.ReadObjectAsMap(\"networkGroupMap\", delegate (string network, JsonElement jsonGroup)\n            {\n                if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress))\n                    throw new InvalidOperationException(\"Network group map contains an invalid network address: \" + network);\n\n                return new Tuple<NetworkAddress, string>(networkAddress, jsonGroup.GetString() ?? \"\");\n            });\n\n            {\n                Dictionary<Uri, BlockList> allAllowListZones = new Dictionary<Uri, BlockList>(0);\n                Dictionary<Uri, BlockList> allBlockListZones = new Dictionary<Uri, BlockList>(0);\n\n                Dictionary<Uri, RegexList> allRegexAllowListZones = new Dictionary<Uri, RegexList>(0);\n                Dictionary<Uri, RegexList> allRegexBlockListZones = new Dictionary<Uri, RegexList>(0);\n\n                Dictionary<Uri, AdBlockList> allAdBlockListZones = new Dictionary<Uri, AdBlockList>(0);\n\n                _groups = jsonConfig.ReadArrayAsMap(\"groups\", delegate (JsonElement jsonGroup)\n                {\n                    Group group = new Group(this, jsonGroup);\n\n                    foreach (Uri allowListUrl in group.AllowListUrls)\n                    {\n                        if (!allAllowListZones.ContainsKey(allowListUrl))\n                        {\n                            if (_allAllowListZones.TryGetValue(allowListUrl, out BlockList? allowList))\n                                allAllowListZones.Add(allowListUrl, allowList);\n                            else\n                                allAllowListZones.Add(allowListUrl, new BlockList(_dnsServer, allowListUrl, true));\n                        }\n                    }\n\n                    foreach (UrlEntry blockListUrl in group.BlockListUrls)\n                    {\n                        if (!allBlockListZones.ContainsKey(blockListUrl.Uri!))\n                        {\n                            if (_allBlockListZones.TryGetValue(blockListUrl.Uri!, out BlockList? blockList))\n                                allBlockListZones.Add(blockListUrl.Uri!, blockList);\n                            else\n                                allBlockListZones.Add(blockListUrl.Uri!, new BlockList(_dnsServer, blockListUrl.Uri!, false));\n                        }\n                    }\n\n                    foreach (Uri regexAllowListUrl in group.RegexAllowListUrls)\n                    {\n                        if (!allRegexAllowListZones.ContainsKey(regexAllowListUrl))\n                        {\n                            if (_allRegexAllowListZones.TryGetValue(regexAllowListUrl, out RegexList? regexAllowList))\n                                allRegexAllowListZones.Add(regexAllowListUrl, regexAllowList);\n                            else\n                                allRegexAllowListZones.Add(regexAllowListUrl, new RegexList(_dnsServer, regexAllowListUrl, true));\n                        }\n                    }\n\n                    foreach (UrlEntry regexBlockListUrl in group.RegexBlockListUrls)\n                    {\n                        if (!allRegexBlockListZones.ContainsKey(regexBlockListUrl.Uri!))\n                        {\n                            if (_allRegexBlockListZones.TryGetValue(regexBlockListUrl.Uri!, out RegexList? regexBlockList))\n                                allRegexBlockListZones.Add(regexBlockListUrl.Uri!, regexBlockList);\n                            else\n                                allRegexBlockListZones.Add(regexBlockListUrl.Uri!, new RegexList(_dnsServer, regexBlockListUrl.Uri!, false));\n                        }\n                    }\n\n                    foreach (UrlEntry adblockListUrl in group.AdblockListUrls)\n                    {\n                        if (!allAdBlockListZones.ContainsKey(adblockListUrl.Uri!))\n                        {\n                            if (_allAdBlockListZones.TryGetValue(adblockListUrl.Uri!, out AdBlockList? adBlockList))\n                                allAdBlockListZones.Add(adblockListUrl.Uri!, adBlockList);\n                            else\n                                allAdBlockListZones.Add(adblockListUrl.Uri!, new AdBlockList(_dnsServer, adblockListUrl.Uri!));\n                        }\n                    }\n\n                    return new Tuple<string, Group>(group.Name, group);\n                });\n\n                _allAllowListZones = allAllowListZones;\n                _allBlockListZones = allBlockListZones;\n\n                _allRegexAllowListZones = allRegexAllowListZones;\n                _allRegexBlockListZones = allRegexBlockListZones;\n\n                _allAdBlockListZones = allAdBlockListZones;\n            }\n\n            foreach (KeyValuePair<string, Group> group in _groups)\n            {\n                group.Value.LoadListZones();\n                _dnsServer.WriteLog(\"Advanced Blocking app loaded all zones successfully for group: \" + group.Key);\n            }\n\n            ThreadPool.QueueUserWorkItem(async delegate (object? state)\n            {\n                try\n                {\n                    List<Task> loadTasks = new List<Task>();\n\n                    foreach (KeyValuePair<Uri, BlockList> allAllowListZone in _allAllowListZones)\n                        loadTasks.Add(allAllowListZone.Value.LoadAsync());\n\n                    foreach (KeyValuePair<Uri, BlockList> allBlockListZone in _allBlockListZones)\n                        loadTasks.Add(allBlockListZone.Value.LoadAsync());\n\n                    foreach (KeyValuePair<Uri, RegexList> allRegexAllowListZone in _allRegexAllowListZones)\n                        loadTasks.Add(allRegexAllowListZone.Value.LoadAsync());\n\n                    foreach (KeyValuePair<Uri, RegexList> allRegexBlockListZone in _allRegexBlockListZones)\n                        loadTasks.Add(allRegexBlockListZone.Value.LoadAsync());\n\n                    foreach (KeyValuePair<Uri, AdBlockList> allAdBlockListZone in _allAdBlockListZones)\n                        loadTasks.Add(allAdBlockListZone.Value.LoadAsync());\n\n                    await Task.WhenAll(loadTasks);\n\n                    if (_blockListUrlUpdateTimer is null)\n                    {\n                        DateTime latest = DateTime.MinValue;\n\n                        foreach (KeyValuePair<Uri, BlockList> allAllowListZone in _allAllowListZones)\n                        {\n                            if (allAllowListZone.Value.LastModified > latest)\n                                latest = allAllowListZone.Value.LastModified;\n                        }\n\n                        foreach (KeyValuePair<Uri, BlockList> allBlockListZone in _allBlockListZones)\n                        {\n                            if (allBlockListZone.Value.LastModified > latest)\n                                latest = allBlockListZone.Value.LastModified;\n                        }\n\n                        foreach (KeyValuePair<Uri, RegexList> allRegexAllowListZone in _allRegexAllowListZones)\n                        {\n                            if (allRegexAllowListZone.Value.LastModified > latest)\n                                latest = allRegexAllowListZone.Value.LastModified;\n                        }\n\n                        foreach (KeyValuePair<Uri, RegexList> allRegexBlockListZone in _allRegexBlockListZones)\n                        {\n                            if (allRegexBlockListZone.Value.LastModified > latest)\n                                latest = allRegexBlockListZone.Value.LastModified;\n                        }\n\n                        foreach (KeyValuePair<Uri, AdBlockList> allAdBlockListZone in _allAdBlockListZones)\n                        {\n                            if (allAdBlockListZone.Value.LastModified > latest)\n                                latest = allAdBlockListZone.Value.LastModified;\n                        }\n\n                        _blockListUrlLastUpdatedOn = latest;\n\n                        _blockListUrlUpdateTimer = new Timer(BlockListUrlUpdateTimerCallbackAsync, null, Timeout.Infinite, Timeout.Infinite);\n                        _blockListUrlUpdateTimer.Change(BLOCK_LIST_UPDATE_TIMER_INTERVAL, BLOCK_LIST_UPDATE_TIMER_INTERVAL);\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer?.WriteLog(ex);\n                }\n            });\n\n            if (!jsonConfig.TryGetProperty(\"localEndPointGroupMap\", out _))\n            {\n                config = config.Replace(\"\\\"networkGroupMap\\\"\", \"\\\"localEndPointGroupMap\\\": {\\r\\n  },\\r\\n  \\\"networkGroupMap\\\"\");\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n\n            if (!jsonConfig.TryGetProperty(\"blockingAnswerTtl\", out _))\n            {\n                config = config.Replace(\"\\\"blockListUrlUpdateIntervalHours\\\"\", \"\\\"blockingAnswerTtl\\\": 30,\\r\\n  \\\"blockListUrlUpdateIntervalHours\\\"\");\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n\n            if (!jsonConfig.TryGetProperty(\"blockListUrlUpdateIntervalMinutes\", out _))\n            {\n                config = config.Replace(\"\\\"localEndPointGroupMap\\\"\", \"\\\"blockListUrlUpdateIntervalMinutes\\\": 0,\\r\\n  \\\"localEndPointGroupMap\\\"\");\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n        }\n\n        public Task<bool> IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP)\n        {\n            if (!_enableBlocking)\n                return Task.FromResult(false);\n\n            string? groupName = GetGroupName(request, remoteEP);\n            if ((groupName is null) || !_groups!.TryGetValue(groupName, out Group? group) || !group.EnableBlocking)\n                return Task.FromResult(false);\n\n            DnsQuestionRecord question = request.Question[0];\n\n            return Task.FromResult(group.IsZoneAllowed(question.Name));\n        }\n\n        public Task<DnsDatagram?> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP)\n        {\n            if (!_enableBlocking)\n                return Task.FromResult<DnsDatagram?>(null);\n\n            string? groupName = GetGroupName(request, remoteEP);\n            if ((groupName is null) || !_groups!.TryGetValue(groupName, out Group? group) || !group.EnableBlocking)\n                return Task.FromResult<DnsDatagram?>(null);\n\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!group.IsZoneBlocked(question.Name, out string? blockedDomain, out string? blockedRegex, out UrlEntry? blockListUrl))\n                return Task.FromResult<DnsDatagram?>(null);\n\n            string GetBlockingReport()\n            {\n                string blockingReport = \"source=advanced-blocking-app; group=\" + group.Name;\n\n                if (blockedRegex is null)\n                {\n                    if (blockListUrl!.Uri is not null)\n                        blockingReport += \"; blockListUrl=\" + blockListUrl.Uri.AbsoluteUri + \"; domain=\" + blockedDomain;\n                    else\n                        blockingReport += \"; domain=\" + blockedDomain;\n                }\n                else\n                {\n                    if (blockListUrl!.Uri is not null)\n                        blockingReport += \"; regexBlockListUrl=\" + blockListUrl.Uri.AbsoluteUri + \"; regex=\" + blockedRegex;\n                    else\n                        blockingReport += \"; regex=\" + blockedRegex;\n                }\n\n                return blockingReport;\n            }\n\n            if (group.AllowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT))\n            {\n                //return meta data\n                string blockingReport = GetBlockingReport();\n\n                DnsResourceRecord[] answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _blockingAnswerTtl, new DnsTXTRecordData(blockingReport))];\n\n                return Task.FromResult<DnsDatagram?>(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer));\n            }\n            else\n            {\n                EDnsOption[]? options = null;\n\n                if (group.AllowTxtBlockingReport && (request.EDNS is not null))\n                {\n                    string blockingReport = GetBlockingReport();\n\n                    options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, blockingReport))];\n                }\n\n                DnsResponseCode rcode;\n                IReadOnlyList<DnsResourceRecord>? answer = null;\n                IReadOnlyList<DnsResourceRecord>? authority = null;\n\n                if (blockListUrl!.BlockAsNxDomain)\n                {\n                    rcode = DnsResponseCode.NxDomain;\n\n                    if (blockedDomain is null)\n                        blockedDomain = question.Name;\n\n                    string? parentDomain = GetParentZone(blockedDomain);\n                    if (parentDomain is null)\n                        parentDomain = string.Empty;\n\n                    authority = [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)];\n                }\n                else\n                {\n                    rcode = DnsResponseCode.NoError;\n\n                    switch (question.Type)\n                    {\n                        case DnsResourceRecordType.A:\n                            {\n                                List<DnsResourceRecord> rrList = new List<DnsResourceRecord>(blockListUrl.ARecords.Count);\n\n                                foreach (DnsARecordData record in blockListUrl.ARecords)\n                                    rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _blockingAnswerTtl, record));\n\n                                answer = rrList;\n                            }\n                            break;\n\n                        case DnsResourceRecordType.AAAA:\n                            {\n                                List<DnsResourceRecord> rrList = new List<DnsResourceRecord>(blockListUrl.AAAARecords.Count);\n\n                                foreach (DnsAAAARecordData record in blockListUrl.AAAARecords)\n                                    rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _blockingAnswerTtl, record));\n\n                                answer = rrList;\n                            }\n                            break;\n\n                        case DnsResourceRecordType.NS:\n                            if (blockedDomain is null)\n                                blockedDomain = question.Name;\n\n                            if (question.Name.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase))\n                                answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.NS, question.Class, _blockingAnswerTtl, _nsRecord)];\n                            else\n                                authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)];\n\n                            break;\n\n                        case DnsResourceRecordType.SOA:\n                            if (blockedDomain is null)\n                                blockedDomain = question.Name;\n\n                            answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)];\n                            break;\n\n                        default:\n                            if (blockedDomain is null)\n                                blockedDomain = question.Name;\n\n                            authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)];\n                            break;\n                    }\n                }\n\n                return Task.FromResult<DnsDatagram?>(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));\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        #endregion\n\n        class UrlEntry\n        {\n            #region variables\n\n            readonly Uri? _uri;\n            readonly bool _blockAsNxDomain;\n\n            readonly List<DnsARecordData> _aRecords;\n            readonly List<DnsAAAARecordData> _aaaaRecords;\n\n            #endregion\n\n            #region constructor\n\n            public UrlEntry(Uri? uri, Group group)\n            {\n                _uri = uri;\n                _blockAsNxDomain = group.BlockAsNxDomain;\n                _aRecords = group.ARecords;\n                _aaaaRecords = group.AAAARecords;\n            }\n\n            public UrlEntry(JsonElement jsonUrl, Group group)\n            {\n                switch (jsonUrl.ValueKind)\n                {\n                    case JsonValueKind.String:\n                        _uri = new Uri(jsonUrl.GetString()!);\n\n                        _blockAsNxDomain = group.BlockAsNxDomain;\n                        _aRecords = group.ARecords;\n                        _aaaaRecords = group.AAAARecords;\n                        break;\n\n                    case JsonValueKind.Object:\n                        _uri = new Uri(jsonUrl.GetProperty(\"url\").GetString()!);\n\n                        if (jsonUrl.TryGetProperty(\"blockAsNxDomain\", out JsonElement jsonBlockAsNxDomain))\n                            _blockAsNxDomain = jsonBlockAsNxDomain.GetBoolean();\n                        else\n                            _blockAsNxDomain = group.BlockAsNxDomain;\n\n                        if (jsonUrl.TryGetProperty(\"blockingAddresses\", out JsonElement jsonBlockingAddresses))\n                        {\n                            List<DnsARecordData> aRecords = new List<DnsARecordData>();\n                            List<DnsAAAARecordData> aaaaRecords = new List<DnsAAAARecordData>();\n\n                            foreach (JsonElement jsonBlockingAddress in jsonBlockingAddresses.EnumerateArray())\n                            {\n                                string? strAddress = jsonBlockingAddress.GetString();\n\n                                if (IPAddress.TryParse(strAddress, out IPAddress? address))\n                                {\n                                    switch (address.AddressFamily)\n                                    {\n                                        case AddressFamily.InterNetwork:\n                                            aRecords.Add(new DnsARecordData(address));\n                                            break;\n\n                                        case AddressFamily.InterNetworkV6:\n                                            aaaaRecords.Add(new DnsAAAARecordData(address));\n                                            break;\n                                    }\n                                }\n                            }\n\n                            _aRecords = aRecords.Count > 0 ? aRecords : group.ARecords;\n                            _aaaaRecords = aaaaRecords.Count > 0 ? aaaaRecords : group.AAAARecords;\n                        }\n                        else\n                        {\n                            _aRecords = group.ARecords;\n                            _aaaaRecords = group.AAAARecords;\n                        }\n\n                        break;\n\n                    default:\n                        throw new InvalidDataException(\"Unexpected URL format: \" + jsonUrl.ValueKind);\n                }\n            }\n\n            #endregion\n\n            #region properties\n\n            public Uri? Uri\n            { get { return _uri; } }\n\n            public bool BlockAsNxDomain\n            { get { return _blockAsNxDomain; } }\n\n            public List<DnsARecordData> ARecords\n            { get { return _aRecords; } }\n\n            public List<DnsAAAARecordData> AAAARecords\n            { get { return _aaaaRecords; } }\n\n            #endregion\n        }\n\n        class ListZoneEntry<T> where T : ListBase\n        {\n            #region variables\n\n            readonly UrlEntry _urlEntry;\n            readonly T _list;\n\n            #endregion\n\n            #region constructor\n\n            public ListZoneEntry(UrlEntry urlEntry, T list)\n            {\n                _urlEntry = urlEntry;\n                _list = list;\n            }\n\n            #endregion\n\n            #region public\n\n            public UrlEntry UrlEntry\n            { get { return _urlEntry; } }\n\n            public T List\n            { get { return _list; } }\n\n            #endregion\n        }\n\n        class Group\n        {\n            #region variables\n\n            readonly App _app;\n\n            readonly string _name;\n            readonly bool _enableBlocking;\n            readonly bool _allowTxtBlockingReport;\n            readonly bool _blockAsNxDomain;\n\n            readonly List<DnsARecordData> _aRecords;\n            readonly List<DnsAAAARecordData> _aaaaRecords;\n\n            readonly HashSet<string> _allowed;\n            readonly HashSet<string> _blocked;\n            readonly Uri[] _allowListUrls;\n            readonly UrlEntry[] _blockListUrls;\n\n            readonly Regex[] _allowedRegex;\n            readonly Regex[] _blockedRegex;\n            readonly Uri[] _regexAllowListUrls;\n            readonly UrlEntry[] _regexBlockListUrls;\n\n            readonly UrlEntry[] _adblockListUrls;\n\n            Dictionary<Uri, BlockList> _allowListZones = [];\n            Dictionary<Uri, ListZoneEntry<BlockList>> _blockListZones = [];\n\n            Dictionary<Uri, RegexList> _regexAllowListZones = [];\n            Dictionary<Uri, ListZoneEntry<RegexList>> _regexBlockListZones = [];\n\n            Dictionary<Uri, ListZoneEntry<AdBlockList>> _adBlockListZones = [];\n\n            #endregion\n\n            #region constructor\n\n            public Group(App app, JsonElement jsonGroup)\n            {\n                _app = app;\n\n                _name = jsonGroup.GetProperty(\"name\").GetString()!;\n                _enableBlocking = jsonGroup.GetPropertyValue(\"enableBlocking\", true);\n                _allowTxtBlockingReport = jsonGroup.GetPropertyValue(\"allowTxtBlockingReport\", true);\n                _blockAsNxDomain = jsonGroup.GetPropertyValue(\"blockAsNxDomain\", false);\n\n                if (jsonGroup.TryGetProperty(\"blockingAddresses\", out JsonElement jsonBlockingAddresses))\n                {\n                    List<DnsARecordData> aRecords = new List<DnsARecordData>();\n                    List<DnsAAAARecordData> aaaaRecords = new List<DnsAAAARecordData>();\n\n                    foreach (JsonElement jsonBlockingAddress in jsonBlockingAddresses.EnumerateArray())\n                    {\n                        string? strAddress = jsonBlockingAddress.GetString();\n\n                        if (IPAddress.TryParse(strAddress, out IPAddress? address))\n                        {\n                            switch (address.AddressFamily)\n                            {\n                                case AddressFamily.InterNetwork:\n                                    aRecords.Add(new DnsARecordData(address));\n                                    break;\n\n                                case AddressFamily.InterNetworkV6:\n                                    aaaaRecords.Add(new DnsAAAARecordData(address));\n                                    break;\n                            }\n                        }\n                    }\n\n                    _aRecords = aRecords;\n                    _aaaaRecords = aaaaRecords;\n                }\n                else\n                {\n                    _aRecords = [];\n                    _aaaaRecords = [];\n                }\n\n                _allowed = jsonGroup.ReadArrayAsSet(\"allowed\");\n                _blocked = jsonGroup.ReadArrayAsSet(\"blocked\");\n                _allowListUrls = jsonGroup.ReadArray(\"allowListUrls\", GetUriEntry);\n                _blockListUrls = jsonGroup.ReadArray(\"blockListUrls\", GetUrlEntry);\n\n                _allowedRegex = jsonGroup.ReadArray(\"allowedRegex\", GetRegexEntry);\n                _blockedRegex = jsonGroup.ReadArray(\"blockedRegex\", GetRegexEntry);\n                _regexAllowListUrls = jsonGroup.ReadArray(\"regexAllowListUrls\", GetUriEntry);\n                _regexBlockListUrls = jsonGroup.ReadArray(\"regexBlockListUrls\", GetUrlEntry);\n\n                _adblockListUrls = jsonGroup.ReadArray(\"adblockListUrls\", GetUrlEntry);\n            }\n\n            #endregion\n\n            #region private\n\n            private static Uri GetUriEntry(string uriString)\n            {\n                return new Uri(uriString);\n            }\n\n            private UrlEntry GetUrlEntry(JsonElement jsonUrl)\n            {\n                return new UrlEntry(jsonUrl, this);\n            }\n\n            private static Regex GetRegexEntry(string pattern)\n            {\n                return new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);\n            }\n\n            #endregion\n\n            #region public\n\n            public void LoadListZones()\n            {\n                {\n                    Dictionary<Uri, BlockList> allowListZones = new Dictionary<Uri, BlockList>(_allowListUrls.Length);\n\n                    foreach (Uri listUrl in _allowListUrls)\n                    {\n                        if (_app._allAllowListZones.TryGetValue(listUrl, out BlockList? allowListZone))\n                            allowListZones.Add(listUrl, allowListZone);\n                    }\n\n                    _allowListZones = allowListZones;\n                }\n\n                {\n                    Dictionary<Uri, ListZoneEntry<BlockList>> blockListZones = new Dictionary<Uri, ListZoneEntry<BlockList>>(_blockListUrls.Length);\n\n                    foreach (UrlEntry listUrl in _blockListUrls)\n                    {\n                        if (_app._allBlockListZones.TryGetValue(listUrl.Uri!, out BlockList? blockListZone))\n                            blockListZones.Add(listUrl.Uri!, new ListZoneEntry<BlockList>(listUrl, blockListZone));\n                    }\n\n                    _blockListZones = blockListZones;\n                }\n\n                {\n                    Dictionary<Uri, RegexList> regexAllowListZones = new Dictionary<Uri, RegexList>(_regexAllowListUrls.Length);\n\n                    foreach (Uri listUrl in _regexAllowListUrls)\n                    {\n                        if (_app._allRegexAllowListZones.TryGetValue(listUrl, out RegexList? regexAllowListZone))\n                            regexAllowListZones.Add(listUrl, regexAllowListZone);\n                    }\n\n                    _regexAllowListZones = regexAllowListZones;\n                }\n\n                {\n                    Dictionary<Uri, ListZoneEntry<RegexList>> regexBlockListZones = new Dictionary<Uri, ListZoneEntry<RegexList>>(_regexBlockListUrls.Length);\n\n                    foreach (UrlEntry listUrl in _regexBlockListUrls)\n                    {\n                        if (_app._allRegexBlockListZones.TryGetValue(listUrl.Uri!, out RegexList? regexBlockListZone))\n                            regexBlockListZones.Add(listUrl.Uri!, new ListZoneEntry<RegexList>(listUrl, regexBlockListZone));\n                    }\n\n                    _regexBlockListZones = regexBlockListZones;\n                }\n\n                {\n                    Dictionary<Uri, ListZoneEntry<AdBlockList>> adBlockListZones = new Dictionary<Uri, ListZoneEntry<AdBlockList>>(_adblockListUrls.Length);\n\n                    foreach (UrlEntry listUrl in _adblockListUrls)\n                    {\n                        if (_app._allAdBlockListZones.TryGetValue(listUrl.Uri!, out AdBlockList? adBlockListZone))\n                            adBlockListZones.Add(listUrl.Uri!, new ListZoneEntry<AdBlockList>(listUrl, adBlockListZone));\n                    }\n\n                    _adBlockListZones = adBlockListZones;\n                }\n            }\n\n            public bool IsZoneAllowed(string domain)\n            {\n                domain = domain.ToLowerInvariant();\n\n                //allowed, allow list zone, allowedRegex, regex allow list zone, adblock list zone\n                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 _);\n            }\n\n            public bool IsZoneBlocked(string domain, out string? blockedDomain, out string? blockedRegex, out UrlEntry? listUrl)\n            {\n                domain = domain.ToLowerInvariant();\n\n                //blocked\n                if (IsZoneFound(_blocked, domain, out string? foundZone1))\n                {\n                    //found zone blocked\n                    blockedDomain = foundZone1;\n                    blockedRegex = null;\n                    listUrl = new UrlEntry(null, this);\n                    return true;\n                }\n\n                //block list zone\n                if (IsZoneFound(_blockListZones, domain, out string? foundZone2, out UrlEntry? blockListUrl1))\n                {\n                    //found zone blocked\n                    blockedDomain = foundZone2;\n                    blockedRegex = null;\n                    listUrl = blockListUrl1;\n                    return true;\n                }\n\n                //blockedRegex\n                if (IsMatchFound(_blockedRegex, domain, out string? blockedPattern1))\n                {\n                    //found pattern blocked\n                    blockedDomain = null;\n                    blockedRegex = blockedPattern1;\n                    listUrl = new UrlEntry(null, this);\n                    return true;\n                }\n\n                //regex block list zone\n                if (IsMatchFound(_regexBlockListZones, domain, out string? blockedPattern2, out UrlEntry? blockListUrl2))\n                {\n                    //found pattern blocked\n                    blockedDomain = null;\n                    blockedRegex = blockedPattern2;\n                    listUrl = blockListUrl2;\n                    return true;\n                }\n\n                //adblock list zone\n                if (App.IsZoneBlocked(_adBlockListZones, domain, out string? foundZone3, out UrlEntry? blockListUrl3))\n                {\n                    //found zone blocked\n                    blockedDomain = foundZone3;\n                    blockedRegex = null;\n                    listUrl = blockListUrl3;\n                    return true;\n                }\n\n                blockedDomain = null;\n                blockedRegex = null;\n                listUrl = null;\n                return false;\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public bool EnableBlocking\n            { get { return _enableBlocking; } }\n\n            public bool AllowTxtBlockingReport\n            { get { return _allowTxtBlockingReport; } }\n\n            public bool BlockAsNxDomain\n            { get { return _blockAsNxDomain; } }\n\n            public List<DnsARecordData> ARecords\n            { get { return _aRecords; } }\n\n            public List<DnsAAAARecordData> AAAARecords\n            { get { return _aaaaRecords; } }\n\n            public Uri[] AllowListUrls\n            { get { return _allowListUrls; } }\n\n            public UrlEntry[] BlockListUrls\n            { get { return _blockListUrls; } }\n\n            public UrlEntry[] RegexBlockListUrls\n            { get { return _regexBlockListUrls; } }\n\n            public Uri[] RegexAllowListUrls\n            { get { return _regexAllowListUrls; } }\n\n            public UrlEntry[] AdblockListUrls\n            { get { return _adblockListUrls; } }\n\n            #endregion\n        }\n\n        abstract class ListBase\n        {\n            #region variables\n\n            protected readonly IDnsServer _dnsServer;\n            protected readonly Uri _listUrl;\n            protected readonly bool _isAllowList;\n            protected readonly bool _isRegexList;\n            protected readonly bool _isAdblockList;\n\n            protected readonly string _listFilePath;\n            bool _listZoneLoaded;\n            DateTime _lastModified;\n\n            volatile bool _isLoading;\n\n            #endregion\n\n            #region constructor\n\n            public ListBase(IDnsServer dnsServer, Uri listUrl, bool isAllowList, bool isRegexList, bool isAdblockList)\n            {\n                _dnsServer = dnsServer;\n                _listUrl = listUrl;\n                _isAllowList = isAllowList;\n                _isRegexList = isRegexList;\n                _isAdblockList = isAdblockList;\n\n                _listFilePath = Path.Combine(Path.Combine(_dnsServer.ApplicationFolder, \"blocklists\"), Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(_listUrl.AbsoluteUri))).ToLowerInvariant());\n            }\n\n            #endregion\n\n            #region private\n\n            private async Task<bool> DownloadListFileAsync()\n            {\n                try\n                {\n                    _dnsServer.WriteLog(\"Advanced Blocking app is downloading \" + (_isAdblockList ? \"adblock\" : (_isRegexList ? \"regex \" : \"\") + (_isAllowList ? \"allow\" : \"block\")) + \" list: \" + _listUrl.AbsoluteUri);\n\n                    if (_listUrl.IsFile)\n                    {\n                        if (File.Exists(_listFilePath))\n                        {\n                            if (File.GetLastWriteTimeUtc(_listUrl.LocalPath) <= File.GetLastWriteTimeUtc(_listFilePath))\n                            {\n                                _dnsServer.WriteLog(\"Advanced Blocking app successfully checked for a new update of the \" + (_isAdblockList ? \"adblock\" : (_isRegexList ? \"regex \" : \"\") + (_isAllowList ? \"allow\" : \"block\")) + \" list: \" + _listUrl.AbsoluteUri);\n                                return false;\n                            }\n                        }\n\n                        File.Copy(_listUrl.LocalPath, _listFilePath, true);\n                        _lastModified = File.GetLastWriteTimeUtc(_listFilePath);\n\n                        _dnsServer.WriteLog(\"Advanced Blocking app successfully downloaded \" + (_isAdblockList ? \"adblock\" : (_isRegexList ? \"regex \" : \"\") + (_isAllowList ? \"allow\" : \"block\")) + \" list (\" + WebUtilities.GetFormattedSize(new FileInfo(_listFilePath).Length) + \"): \" + _listUrl.AbsoluteUri);\n                        return true;\n                    }\n                    else\n                    {\n                        HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n                        handler.Proxy = _dnsServer.Proxy;\n                        handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                        handler.DnsClient = _dnsServer;\n\n                        using (HttpClient http = new HttpClient(handler))\n                        {\n                            if (File.Exists(_listFilePath))\n                                http.DefaultRequestHeaders.IfModifiedSince = File.GetLastWriteTimeUtc(_listFilePath);\n\n                            HttpResponseMessage httpResponse = await http.GetAsync(_listUrl);\n                            switch (httpResponse.StatusCode)\n                            {\n                                case HttpStatusCode.OK:\n                                    string listDownloadFilePath = _listFilePath + \".downloading\";\n\n                                    using (FileStream fS = new FileStream(listDownloadFilePath, FileMode.Create, FileAccess.Write))\n                                    {\n                                        using (Stream httpStream = await httpResponse.Content.ReadAsStreamAsync())\n                                        {\n                                            await httpStream.CopyToAsync(fS);\n                                        }\n                                    }\n\n                                    File.Move(listDownloadFilePath, _listFilePath, true);\n\n                                    if (httpResponse.Content.Headers.LastModified is null)\n                                    {\n                                        _lastModified = DateTime.UtcNow;\n                                    }\n                                    else\n                                    {\n                                        _lastModified = httpResponse.Content.Headers.LastModified.Value.UtcDateTime;\n                                        File.SetLastWriteTimeUtc(_listFilePath, _lastModified);\n                                    }\n\n                                    _dnsServer.WriteLog(\"Advanced Blocking app successfully downloaded \" + (_isAdblockList ? \"adblock\" : (_isRegexList ? \"regex \" : \"\") + (_isAllowList ? \"allow\" : \"block\")) + \" list (\" + WebUtilities.GetFormattedSize(new FileInfo(_listFilePath).Length) + \"): \" + _listUrl.AbsoluteUri);\n                                    return true;\n\n                                case HttpStatusCode.NotModified:\n                                    _dnsServer.WriteLog(\"Advanced Blocking app successfully checked for a new update of the \" + (_isAdblockList ? \"adblock\" : (_isRegexList ? \"regex \" : \"\") + (_isAllowList ? \"allow\" : \"block\")) + \" list: \" + _listUrl.AbsoluteUri);\n                                    return false;\n\n                                default:\n                                    throw new HttpRequestException((int)httpResponse.StatusCode + \" \" + httpResponse.ReasonPhrase);\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _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());\n                    return false;\n                }\n            }\n\n            #endregion\n\n            #region protected\n\n            protected abstract void LoadListZone();\n\n            #endregion\n\n            #region public\n\n            public async Task LoadAsync()\n            {\n                if (_isLoading)\n                    return;\n\n                _isLoading = true;\n\n                try\n                {\n                    if (File.Exists(_listFilePath))\n                    {\n                        _lastModified = File.GetLastWriteTimeUtc(_listFilePath);\n\n                        if (_listUrl.IsFile && (File.GetLastWriteTimeUtc(_listUrl.LocalPath) > _lastModified))\n                        {\n                            File.Copy(_listUrl.LocalPath, _listFilePath, true);\n                            _lastModified = File.GetLastWriteTimeUtc(_listFilePath);\n\n                            _dnsServer.WriteLog(\"Advanced Blocking app successfully downloaded \" + (_isAdblockList ? \"adblock\" : (_isRegexList ? \"regex \" : \"\") + (_isAllowList ? \"allow\" : \"block\")) + \" list (\" + WebUtilities.GetFormattedSize(new FileInfo(_listFilePath).Length) + \"): \" + _listUrl.AbsoluteUri);\n\n                            LoadListZone();\n                            _listZoneLoaded = true;\n                        }\n                        else if (!_listZoneLoaded)\n                        {\n                            LoadListZone();\n                            _listZoneLoaded = true;\n                        }\n                    }\n                    else\n                    {\n                        if (await DownloadListFileAsync())\n                        {\n                            LoadListZone();\n                            _listZoneLoaded = true;\n                        }\n                    }\n                }\n                finally\n                {\n                    _isLoading = false;\n                }\n            }\n\n            public async Task<bool> UpdateAsync()\n            {\n                if (await DownloadListFileAsync())\n                {\n                    LoadListZone();\n                    return true;\n                }\n\n                return false;\n            }\n\n            #endregion\n\n            #region properties\n\n            public DateTime LastModified\n            { get { return _lastModified; } }\n\n            #endregion\n        }\n\n        class BlockList : ListBase\n        {\n            #region variables\n\n            readonly static char[] _popWordSeperator = new char[] { ' ', '\\t' };\n\n            HashSet<string> _listZone = [];\n\n            #endregion\n\n            #region constructor\n\n            public BlockList(IDnsServer dnsServer, Uri listUrl, bool isAllowList)\n                : base(dnsServer, listUrl, isAllowList, false, false)\n            { }\n\n            #endregion\n\n            #region private\n\n            private static string PopWord(ref string line)\n            {\n                if (line.Length == 0)\n                    return line;\n\n                line = line.TrimStart(_popWordSeperator);\n\n                int i = line.IndexOfAny(_popWordSeperator);\n                string word;\n\n                if (i < 0)\n                {\n                    word = line;\n                    line = \"\";\n                }\n                else\n                {\n                    word = line.Substring(0, i);\n                    line = line.Substring(i + 1);\n                }\n\n                return word;\n            }\n\n            private Queue<string> ReadListFile()\n            {\n                Queue<string> domains = new Queue<string>();\n\n                try\n                {\n                    _dnsServer.WriteLog(\"Advanced Blocking app is reading \" + (_isAllowList ? \"allow\" : \"block\") + \" list from: \" + _listUrl.AbsoluteUri);\n\n                    using (FileStream fS = new FileStream(_listFilePath, FileMode.Open, FileAccess.Read))\n                    {\n                        //parse hosts file and populate block zone\n                        StreamReader sR = new StreamReader(fS, true);\n                        char[] trimSeperator = new char[] { ' ', '\\t', '*', '.' };\n                        string? line;\n                        string firstWord;\n                        string secondWord;\n                        string hostname;\n\n                        while (true)\n                        {\n                            line = sR.ReadLine();\n                            if (line is null)\n                                break; //eof\n\n                            line = line.TrimStart(trimSeperator);\n\n                            if (line.Length == 0)\n                                continue; //skip empty line\n\n                            if (line.StartsWith('#'))\n                                continue; //skip comment line\n\n                            firstWord = PopWord(ref line);\n\n                            if (line.Length == 0)\n                            {\n                                hostname = firstWord;\n                            }\n                            else\n                            {\n                                secondWord = PopWord(ref line);\n\n                                if ((secondWord.Length == 0) || secondWord.StartsWith('#'))\n                                    hostname = firstWord;\n                                else\n                                    hostname = secondWord;\n                            }\n\n                            hostname = hostname.Trim('.').ToLowerInvariant();\n\n                            switch (hostname)\n                            {\n                                case \"\":\n                                case \"localhost\":\n                                case \"localhost.localdomain\":\n                                case \"local\":\n                                case \"broadcasthost\":\n                                case \"ip6-localhost\":\n                                case \"ip6-loopback\":\n                                case \"ip6-localnet\":\n                                case \"ip6-mcastprefix\":\n                                case \"ip6-allnodes\":\n                                case \"ip6-allrouters\":\n                                case \"ip6-allhosts\":\n                                    continue; //skip these hostnames\n                            }\n\n                            if (!DnsClient.IsDomainNameValid(hostname))\n                                continue;\n\n                            if (IPAddress.TryParse(hostname, out _))\n                                continue; //skip line when hostname is IP address\n\n                            domains.Enqueue(hostname);\n                        }\n                    }\n\n                    _dnsServer.WriteLog(\"Advanced Blocking app read \" + (_isAllowList ? \"allow\" : \"block\") + \" list file (\" + domains.Count + \" domains) from: \" + _listUrl.AbsoluteUri);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(\"Advanced Blocking app failed to read \" + (_isAllowList ? \"allow\" : \"block\") + \" list from: \" + _listUrl.AbsoluteUri + \"\\r\\n\" + ex.ToString());\n                }\n\n                return domains;\n            }\n\n            #endregion\n\n            #region protected\n\n            protected override void LoadListZone()\n            {\n                Queue<string> listQueue = ReadListFile();\n                HashSet<string> listZone = new HashSet<string>(listQueue.Count);\n\n                while (listQueue.Count > 0)\n                    listZone.Add(listQueue.Dequeue());\n\n                _listZone = listZone;\n            }\n\n            #endregion\n\n            #region public\n\n            public bool IsZoneFound(string domain, out string? foundZone)\n            {\n                return App.IsZoneFound(_listZone, domain, out foundZone);\n            }\n\n            #endregion\n        }\n\n        class RegexList : ListBase\n        {\n            #region variables\n\n            IReadOnlyList<Regex> _regexListZone = [];\n\n            #endregion\n\n            #region constructor\n\n            public RegexList(IDnsServer dnsServer, Uri listUrl, bool isAllowList)\n                : base(dnsServer, listUrl, isAllowList, true, false)\n            { }\n\n            #endregion\n\n            #region private\n\n            private Queue<string> ReadRegexListFile()\n            {\n                Queue<string> regices = new Queue<string>();\n\n                try\n                {\n                    _dnsServer.WriteLog(\"Advanced Blocking app is reading regex \" + (_isAllowList ? \"allow\" : \"block\") + \" list from: \" + _listUrl.AbsoluteUri);\n\n                    using (FileStream fS = new FileStream(_listFilePath, FileMode.Open, FileAccess.Read))\n                    {\n                        //parse hosts file and populate block zone\n                        StreamReader sR = new StreamReader(fS, true);\n                        char[] trimSeperator = new char[] { ' ', '\\t' };\n                        string? line;\n\n                        while (true)\n                        {\n                            line = sR.ReadLine();\n                            if (line is null)\n                                break; //eof\n\n                            line = line.TrimStart(trimSeperator);\n\n                            if (line.Length == 0)\n                                continue; //skip empty line\n\n                            if (line.StartsWith('#'))\n                                continue; //skip comment line\n\n                            regices.Enqueue(line);\n                        }\n                    }\n\n                    _dnsServer.WriteLog(\"Advanced Blocking app read regex \" + (_isAllowList ? \"allow\" : \"block\") + \" list file (\" + regices.Count + \" regex patterns) from: \" + _listUrl.AbsoluteUri);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(\"Advanced Blocking app failed to read regex \" + (_isAllowList ? \"allow\" : \"block\") + \" list from: \" + _listUrl.AbsoluteUri + \"\\r\\n\" + ex.ToString());\n                }\n\n                return regices;\n            }\n\n            #endregion\n\n            #region protected\n\n            protected override void LoadListZone()\n            {\n                Queue<string> regexPatterns = ReadRegexListFile();\n                List<Regex> regexListZone = new List<Regex>(regexPatterns.Count);\n\n                while (regexPatterns.Count > 0)\n                {\n                    try\n                    {\n                        regexListZone.Add(new Regex(regexPatterns.Dequeue(), RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled));\n                    }\n                    catch (RegexParseException ex)\n                    {\n                        _dnsServer.WriteLog(ex);\n                    }\n                }\n\n                _regexListZone = regexListZone;\n            }\n\n            #endregion\n\n            #region public\n\n            public bool IsMatchFound(string domain, out string? matchingPattern)\n            {\n                return App.IsMatchFound(_regexListZone, domain, out matchingPattern);\n            }\n\n            #endregion\n        }\n\n        class AdBlockList : ListBase\n        {\n            #region variables\n\n            HashSet<string> _allowedListZone = [];\n            HashSet<string> _blockedListZone = [];\n\n            #endregion\n\n            #region constructor\n\n            public AdBlockList(IDnsServer dnsServer, Uri listUrl)\n                : base(dnsServer, listUrl, false, false, true)\n            { }\n\n            #endregion\n\n            #region private\n\n            private void ReadAdblockListFile(out Queue<string> allowedDomains, out Queue<string> blockedDomains)\n            {\n                allowedDomains = new Queue<string>();\n                blockedDomains = new Queue<string>();\n\n                try\n                {\n                    _dnsServer.WriteLog(\"Advanced Blocking app is reading adblock list from: \" + _listUrl.AbsoluteUri);\n\n                    using (FileStream fS = new FileStream(_listFilePath, FileMode.Open, FileAccess.Read))\n                    {\n                        //parse hosts file and populate block zone\n                        StreamReader sR = new StreamReader(fS, true);\n                        char[] trimSeperator = new char[] { ' ', '\\t' };\n                        string? line;\n\n                        while (true)\n                        {\n                            line = sR.ReadLine();\n                            if (line is null)\n                                break; //eof\n\n                            line = line.TrimStart(trimSeperator);\n\n                            if (line.Length == 0)\n                                continue; //skip empty line\n\n                            if (line.StartsWith('!'))\n                                continue; //skip comment line\n\n                            if (line.StartsWith(\"||\"))\n                            {\n                                int i = line.IndexOf('^');\n                                if (i > -1)\n                                {\n                                    string domain = line.Substring(2, i - 2);\n                                    string options = line.Substring(i + 1);\n\n                                    if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains(\"doc\") || options.Contains(\"all\")))) && DnsClient.IsDomainNameValid(domain))\n                                        blockedDomains.Enqueue(domain);\n                                }\n                                else\n                                {\n                                    string domain = line.Substring(2);\n\n                                    if (DnsClient.IsDomainNameValid(domain))\n                                        blockedDomains.Enqueue(domain);\n                                }\n                            }\n                            else if (line.StartsWith(\"@@||\"))\n                            {\n                                int i = line.IndexOf('^');\n                                if (i > -1)\n                                {\n                                    string domain = line.Substring(4, i - 4);\n                                    string options = line.Substring(i + 1);\n\n                                    if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains(\"doc\") || options.Contains(\"all\")))) && DnsClient.IsDomainNameValid(domain))\n                                        allowedDomains.Enqueue(domain);\n                                }\n                                else\n                                {\n                                    string domain = line.Substring(4);\n\n                                    if (DnsClient.IsDomainNameValid(domain))\n                                        allowedDomains.Enqueue(domain);\n                                }\n                            }\n                        }\n                    }\n\n                    _dnsServer.WriteLog(\"Advanced Blocking app read adblock list file (\" + (allowedDomains.Count + blockedDomains.Count) + \" domains) from: \" + _listUrl.AbsoluteUri);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(\"Advanced Blocking app failed to read adblock list from: \" + _listUrl.AbsoluteUri + \"\\r\\n\" + ex.ToString());\n                }\n            }\n\n            #endregion\n\n            #region protected\n\n            protected override void LoadListZone()\n            {\n                ReadAdblockListFile(out Queue<string> allowedDomains, out Queue<string> blockedDomains);\n\n                HashSet<string> allowedListZone = new HashSet<string>(allowedDomains.Count);\n                HashSet<string> blockedListZone = new HashSet<string>(blockedDomains.Count);\n\n                while (allowedDomains.Count > 0)\n                    allowedListZone.Add(allowedDomains.Dequeue());\n\n                while (blockedDomains.Count > 0)\n                    blockedListZone.Add(blockedDomains.Dequeue());\n\n                _allowedListZone = allowedListZone;\n                _blockedListZone = blockedListZone;\n            }\n\n            #endregion\n\n            #region public\n\n            public bool IsZoneAllowed(string domain, out string? foundZone)\n            {\n                return IsZoneFound(_allowedListZone, domain, out foundZone);\n            }\n\n            public bool IsZoneBlocked(string domain, out string? foundZone)\n            {\n                return IsZoneFound(_blockedListZone, domain, out foundZone);\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/AdvancedBlockingApp/dnsApp.config",
    "content": "{\n  \"enableBlocking\": true,\n  \"blockingAnswerTtl\": 30,\n  \"blockListUrlUpdateIntervalHours\": 24,\n  \"blockListUrlUpdateIntervalMinutes\": 0,\n  \"localEndPointGroupMap\": {\n    \"127.0.0.1\": \"bypass\",\n    \"192.168.10.2:53\": \"bypass\",\n    \"user1.dot.example.com\": \"kids\",\n    \"user2.doh.example.com:443\": \"bypass\"\n  },\n  \"networkGroupMap\": {\n    \"192.168.10.20\": \"kids\",\n    \"0.0.0.0/0\": \"everyone\",\n    \"[::]/0\": \"everyone\"\n  },\n  \"groups\": [\n    {\n      \"name\": \"everyone\",\n      \"enableBlocking\": true,\n      \"allowTxtBlockingReport\": true,\n      \"blockAsNxDomain\": true,\n      \"blockingAddresses\": [\n        \"0.0.0.0\",\n        \"::\"\n      ],\n      \"allowed\": [],\n      \"blocked\": [\n        \"example.com\"\n      ],\n      \"allowListUrls\": [],\n      \"blockListUrls\": [\n        \"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\"\n      ],\n      \"allowedRegex\": [],\n      \"blockedRegex\": [\n        \"^ads\\\\.\"\n      ],\n      \"regexAllowListUrls\": [],\n      \"regexBlockListUrls\": [],\n      \"adblockListUrls\": []\n    },\n    {\n      \"name\": \"kids\",\n      \"enableBlocking\": true,\n      \"allowTxtBlockingReport\": true,\n      \"blockAsNxDomain\": true,\n      \"blockingAddresses\": [\n        \"0.0.0.0\",\n        \"::\"\n      ],\n      \"allowed\": [],\n      \"blocked\": [],\n      \"allowListUrls\": [],\n      \"blockListUrls\": [\n        {\n          \"url\": \"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts\",\n          \"blockAsNxDomain\": false,\n          \"blockingAddresses\": [\n            \"192.168.10.2\"\n          ]\n        }\n      ],\n      \"allowedRegex\": [],\n      \"blockedRegex\": [],\n      \"regexAllowListUrls\": [],\n      \"regexBlockListUrls\": [],\n      \"adblockListUrls\": []\n    },\n    {\n      \"name\": \"bypass\",\n      \"enableBlocking\": true,\n      \"allowTxtBlockingReport\": true,\n      \"blockAsNxDomain\": true,\n      \"blockingAddresses\": [\n        \"0.0.0.0\",\n        \"::\"\n      ],\n      \"allowed\": [],\n      \"blocked\": [],\n      \"allowListUrls\": [],\n      \"blockListUrls\": [],\n      \"allowedRegex\": [],\n      \"blockedRegex\": [],\n      \"regexAllowListUrls\": [],\n      \"regexBlockListUrls\": [],\n      \"adblockListUrls\": []\n    }\n  ]\n}"
  },
  {
    "path": "Apps/AdvancedForwardingApp/AdvancedForwardingApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>AdvancedForwardingApp</AssemblyName>\n\t\t<RootNamespace>AdvancedForwarding</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.\\n\\nNote: This app works independent of the DNS server's built-in Conditional Forwarder Zones feature.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"adguard-upstreams.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/AdvancedForwardingApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace AdvancedForwarding\n{\n    public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n\n        byte _appPreference;\n\n        bool _enableForwarding;\n        Dictionary<string, ConfigProxyServer> _configProxyServers;\n        Dictionary<string, ConfigForwarder> _configForwarders;\n        Dictionary<NetworkAddress, string> _networkGroupMap;\n        Dictionary<string, Group> _groups;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            if (_groups is not null)\n            {\n                foreach (KeyValuePair<string, Group> group in _groups)\n                    group.Value.Dispose();\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private static List<DnsForwarderRecordData> GetUpdatedForwarderRecords(IReadOnlyList<DnsForwarderRecordData> forwarderRecords, bool dnssecValidation, ConfigProxyServer configProxyServer)\n        {\n            List<DnsForwarderRecordData> newForwarderRecords = new List<DnsForwarderRecordData>(forwarderRecords.Count);\n\n            foreach (DnsForwarderRecordData forwarderRecord in forwarderRecords)\n                newForwarderRecords.Add(GetForwarderRecord(forwarderRecord.Protocol, forwarderRecord.Forwarder, dnssecValidation, configProxyServer));\n\n            return newForwarderRecords;\n        }\n\n        private static DnsForwarderRecordData GetForwarderRecord(NameServerAddress forwarder, bool dnssecValidation, ConfigProxyServer configProxyServer)\n        {\n            return GetForwarderRecord(forwarder.Protocol, forwarder.ToString(), dnssecValidation, configProxyServer);\n        }\n\n        private static DnsForwarderRecordData GetForwarderRecord(DnsTransportProtocol protocol, string forwarder, bool dnssecValidation, ConfigProxyServer configProxyServer)\n        {\n            DnsForwarderRecordData forwarderRecord;\n\n            if (configProxyServer is null)\n                forwarderRecord = new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, DnsForwarderRecordProxyType.DefaultProxy, null, 0, null, null, 0);\n            else\n                forwarderRecord = new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, configProxyServer.Type, configProxyServer.ProxyAddress, configProxyServer.ProxyPort, configProxyServer.ProxyUsername, configProxyServer.ProxyPassword, 0);\n\n            return forwarderRecord;\n        }\n\n        private Tuple<string, Group> ReadGroup(JsonElement jsonGroup)\n        {\n            string name = jsonGroup.GetProperty(\"name\").GetString();\n\n            if ((_groups is not null) && _groups.TryGetValue(name, out Group group))\n                group.ReloadConfig(_configProxyServers, _configForwarders, jsonGroup);\n            else\n                group = new Group(_dnsServer, _configProxyServers, _configForwarders, jsonGroup);\n\n            return new Tuple<string, Group>(group.Name, group);\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue(\"appPreference\", 200));\n\n            _enableForwarding = jsonConfig.GetPropertyValue(\"enableForwarding\", true);\n\n            if (jsonConfig.TryReadArrayAsMap(\"proxyServers\", delegate (JsonElement jsonProxy)\n            {\n                ConfigProxyServer proxyServer = new ConfigProxyServer(jsonProxy);\n                return new Tuple<string, ConfigProxyServer>(proxyServer.Name, proxyServer);\n            }, out Dictionary<string, ConfigProxyServer> configProxyServers))\n                _configProxyServers = configProxyServers;\n            else\n                _configProxyServers = null;\n\n            if (jsonConfig.TryReadArrayAsMap(\"forwarders\", delegate (JsonElement jsonForwarder)\n            {\n                ConfigForwarder forwarder = new ConfigForwarder(jsonForwarder, _configProxyServers);\n                return new Tuple<string, ConfigForwarder>(forwarder.Name, forwarder);\n            }, out Dictionary<string, ConfigForwarder> configForwarders))\n                _configForwarders = configForwarders;\n            else\n                _configForwarders = null;\n\n            _networkGroupMap = jsonConfig.ReadObjectAsMap(\"networkGroupMap\", delegate (string network, JsonElement jsonGroup)\n            {\n                if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress))\n                    throw new FormatException(\"Network group map contains an invalid network address: \" + network);\n\n                return new Tuple<NetworkAddress, string>(networkAddress, jsonGroup.GetString());\n            });\n\n            if (jsonConfig.TryReadArrayAsMap(\"groups\", ReadGroup, out Dictionary<string, Group> groups))\n            {\n                if (_groups is not null)\n                {\n                    foreach (KeyValuePair<string, Group> group in _groups)\n                    {\n                        if (!groups.ContainsKey(group.Key))\n                            group.Value.Dispose();\n                    }\n                }\n\n                _groups = groups;\n            }\n            else\n            {\n                throw new FormatException(\"Groups array was not defined.\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)\n        {\n            if (!_enableForwarding || !request.RecursionDesired)\n                return Task.FromResult<DnsDatagram>(null);\n\n            IPAddress remoteIP = remoteEP.Address;\n            NetworkAddress network = null;\n            string groupName = null;\n\n            foreach (KeyValuePair<NetworkAddress, string> entry in _networkGroupMap)\n            {\n                if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength)))\n                {\n                    network = entry.Key;\n                    groupName = entry.Value;\n                }\n            }\n\n            if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableForwarding)\n                return Task.FromResult<DnsDatagram>(null);\n\n            DnsQuestionRecord question = request.Question[0];\n            string qname = question.Name;\n\n            if (!group.TryGetForwarderRecords(qname, out IReadOnlyList<DnsForwarderRecordData> forwarderRecords))\n                return Task.FromResult<DnsDatagram>(null);\n\n            request.SetShadowEDnsClientSubnetOption(network, true);\n\n            DnsResourceRecord[] authority = new DnsResourceRecord[forwarderRecords.Count];\n\n            for (int i = 0; i < forwarderRecords.Count; i++)\n                authority[i] = new DnsResourceRecord(qname, DnsResourceRecordType.FWD, DnsClass.IN, 0, forwarderRecords[i]);\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, authority));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Performs bulk conditional forwarding for configured domain names and AdGuard Upstream config files.\"; } }\n\n        public byte Preference\n        { get { return _appPreference; } }\n\n        #endregion\n\n        class Group : IDisposable\n        {\n            #region variables\n\n            readonly IDnsServer _dnsServer;\n            Dictionary<string, ConfigProxyServer> _configProxyServers;\n            Dictionary<string, ConfigForwarder> _configForwarders;\n\n            readonly string _name;\n            bool _enableForwarding;\n            Forwarding[] _forwardings;\n            Dictionary<string, AdGuardUpstream> _adguardUpstreams;\n\n            #endregion\n\n            #region constructor\n\n            public Group(IDnsServer dnsServer, Dictionary<string, ConfigProxyServer> configProxyServers, Dictionary<string, ConfigForwarder> configForwarders, JsonElement jsonGroup)\n            {\n                _dnsServer = dnsServer;\n\n                _name = jsonGroup.GetProperty(\"name\").GetString();\n\n                ReloadConfig(configProxyServers, configForwarders, jsonGroup);\n            }\n\n            #endregion\n\n            #region IDisposable\n\n            public void Dispose()\n            {\n                if (_adguardUpstreams is not null)\n                {\n                    foreach (KeyValuePair<string, AdGuardUpstream> adguardUpstream in _adguardUpstreams)\n                        adguardUpstream.Value.Dispose();\n\n                    _adguardUpstreams = null;\n                }\n            }\n\n            #endregion\n\n            #region private\n\n            private Tuple<string, AdGuardUpstream> ReadAdGuardUpstream(JsonElement jsonAdguardUpstream)\n            {\n                string name = jsonAdguardUpstream.GetProperty(\"configFile\").GetString();\n\n                if ((_adguardUpstreams is not null) && _adguardUpstreams.TryGetValue(name, out AdGuardUpstream adGuardUpstream))\n                    adGuardUpstream.ReloadConfig(_configProxyServers, jsonAdguardUpstream);\n                else\n                    adGuardUpstream = new AdGuardUpstream(_dnsServer, _configProxyServers, jsonAdguardUpstream);\n\n                return new Tuple<string, AdGuardUpstream>(adGuardUpstream.Name, adGuardUpstream);\n            }\n\n            #endregion\n\n            #region public\n\n            public void ReloadConfig(Dictionary<string, ConfigProxyServer> configProxyServers, Dictionary<string, ConfigForwarder> configForwarders, JsonElement jsonGroup)\n            {\n                _configProxyServers = configProxyServers;\n                _configForwarders = configForwarders;\n\n                _enableForwarding = jsonGroup.GetPropertyValue(\"enableForwarding\", true);\n\n                if (jsonGroup.TryReadArray(\"forwardings\", delegate (JsonElement jsonForwarding) { return new Forwarding(jsonForwarding, _configForwarders); }, out Forwarding[] forwardings))\n                    _forwardings = forwardings;\n                else\n                    _forwardings = null;\n\n                if (jsonGroup.TryReadArrayAsMap(\"adguardUpstreams\", ReadAdGuardUpstream, out Dictionary<string, AdGuardUpstream> adguardUpstreams))\n                {\n                    if (_adguardUpstreams is not null)\n                    {\n                        foreach (KeyValuePair<string, AdGuardUpstream> adguardUpstream in _adguardUpstreams)\n                        {\n                            if (!adguardUpstreams.ContainsKey(adguardUpstream.Key))\n                                adguardUpstream.Value.Dispose();\n                        }\n                    }\n\n                    _adguardUpstreams = adguardUpstreams;\n                }\n                else\n                {\n                    if (_adguardUpstreams is not null)\n                    {\n                        foreach (KeyValuePair<string, AdGuardUpstream> adguardUpstream in _adguardUpstreams)\n                            adguardUpstream.Value.Dispose();\n                    }\n\n                    _adguardUpstreams = null;\n                }\n            }\n\n            public bool TryGetForwarderRecords(string domain, out IReadOnlyList<DnsForwarderRecordData> forwarderRecords)\n            {\n                domain = domain.ToLowerInvariant();\n\n                if ((_forwardings is not null) && (_forwardings.Length > 0) && Forwarding.TryGetForwarderRecords(domain, _forwardings, out forwarderRecords))\n                    return true;\n\n                if (_adguardUpstreams is not null)\n                {\n                    foreach (KeyValuePair<string, AdGuardUpstream> adguardUpstream in _adguardUpstreams)\n                    {\n                        if (adguardUpstream.Value.TryGetForwarderRecords(domain, out forwarderRecords))\n                            return true;\n                    }\n                }\n\n                forwarderRecords = null;\n                return false;\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public bool EnableForwarding\n            { get { return _enableForwarding; } }\n\n            #endregion\n        }\n\n        class Forwarding\n        {\n            #region variables\n\n            IReadOnlyList<DnsForwarderRecordData> _forwarderRecords;\n            readonly Dictionary<string, object> _domainMap;\n\n            #endregion\n\n            #region constructor\n\n            public Forwarding(JsonElement jsonForwarding, Dictionary<string, ConfigForwarder> configForwarders)\n            {\n                JsonElement jsonForwarders = jsonForwarding.GetProperty(\"forwarders\");\n                List<DnsForwarderRecordData> forwarderRecords = new List<DnsForwarderRecordData>();\n\n                foreach (JsonElement jsonForwarder in jsonForwarders.EnumerateArray())\n                {\n                    string forwarderName = jsonForwarder.GetString();\n\n                    if ((configForwarders is null) || !configForwarders.TryGetValue(forwarderName, out ConfigForwarder configForwarder))\n                        throw new FormatException(\"Forwarder was not defined: \" + forwarderName);\n\n                    forwarderRecords.AddRange(configForwarder.ForwarderRecords);\n                }\n\n                _forwarderRecords = forwarderRecords;\n\n                _domainMap = jsonForwarding.ReadArrayAsMap(\"domains\", delegate (JsonElement jsonDomain)\n                {\n                    return new Tuple<string, object>(jsonDomain.GetString().ToLowerInvariant(), null);\n                });\n            }\n\n            public Forwarding(IReadOnlyList<string> domains, NameServerAddress forwarder, bool dnssecValidation, ConfigProxyServer proxy)\n                : this(new DnsForwarderRecordData[] { GetForwarderRecord(forwarder, dnssecValidation, proxy) }, domains)\n            { }\n\n            public Forwarding(IReadOnlyList<DnsForwarderRecordData> forwarderRecords, IReadOnlyList<string> domains)\n            {\n                _forwarderRecords = forwarderRecords;\n\n                Dictionary<string, object> domainMap = new Dictionary<string, object>(domains.Count);\n\n                foreach (string domain in domains)\n                {\n                    if (DnsClient.IsDomainNameValid(domain))\n                        domainMap.TryAdd(domain.ToLowerInvariant(), null);\n                }\n\n                _domainMap = domainMap;\n            }\n\n            #endregion\n\n            #region static\n\n            public static bool TryGetForwarderRecords(string domain, IReadOnlyList<Forwarding> forwardings, out IReadOnlyList<DnsForwarderRecordData> forwarderRecords)\n            {\n                if (forwardings.Count == 1)\n                {\n                    if (forwardings[0].TryGetForwarderRecords(domain, out forwarderRecords, out _))\n                        return true;\n                }\n                else\n                {\n                    Dictionary<string, List<DnsForwarderRecordData>> fwdMap = new Dictionary<string, List<DnsForwarderRecordData>>(forwardings.Count);\n\n                    foreach (Forwarding forwarding in forwardings)\n                    {\n                        if (forwarding.TryGetForwarderRecords(domain, out IReadOnlyList<DnsForwarderRecordData> fwdRecords, out string matchedDomain))\n                        {\n                            if (fwdMap.TryGetValue(matchedDomain, out List<DnsForwarderRecordData> fwdRecordsList))\n                            {\n                                fwdRecordsList.AddRange(fwdRecords);\n                            }\n                            else\n                            {\n                                fwdRecordsList = new List<DnsForwarderRecordData>(fwdRecords);\n                                fwdMap.Add(matchedDomain, fwdRecordsList);\n                            }\n                        }\n                    }\n\n                    if (fwdMap.Count > 0)\n                    {\n                        forwarderRecords = null;\n                        string lastMatchedDomain = null;\n\n                        foreach (KeyValuePair<string, List<DnsForwarderRecordData>> fwdEntry in fwdMap)\n                        {\n                            if ((lastMatchedDomain is null) || (fwdEntry.Key.Length > lastMatchedDomain.Length) || ((fwdEntry.Key.Length == lastMatchedDomain.Length) && lastMatchedDomain.StartsWith(\"*.\")))\n                            {\n                                lastMatchedDomain = fwdEntry.Key;\n                                forwarderRecords = fwdEntry.Value;\n                            }\n                        }\n\n                        return true;\n                    }\n                }\n\n                forwarderRecords = null;\n                return false;\n            }\n\n            public static bool IsForwarderDomain(string domain, IReadOnlyList<Forwarding> forwardings)\n            {\n                foreach (Forwarding forwarding in forwardings)\n                {\n                    if (IsForwarderDomain(domain, forwarding._forwarderRecords))\n                        return true;\n                }\n\n                return false;\n            }\n\n            public static bool IsForwarderDomain(string domain, IReadOnlyList<DnsForwarderRecordData> forwarderRecords)\n            {\n                foreach (DnsForwarderRecordData forwarderRecord in forwarderRecords)\n                {\n                    if (domain.Equals(forwarderRecord.NameServer.Host, StringComparison.OrdinalIgnoreCase))\n                        return true;\n                }\n\n                return false;\n            }\n\n            #endregion\n\n            #region private\n\n            private static string GetParentZone(string domain)\n            {\n                int i = domain.IndexOf('.');\n                if (i > -1)\n                    return domain.Substring(i + 1);\n\n                //dont return root zone\n                return null;\n            }\n\n            private bool IsDomainMatching(string domain, out string matchedDomain)\n            {\n                string parent;\n\n                do\n                {\n                    if (_domainMap.TryGetValue(domain, out _))\n                    {\n                        matchedDomain = domain;\n                        return true;\n                    }\n\n                    parent = GetParentZone(domain);\n                    if (parent is null)\n                    {\n                        if (_domainMap.TryGetValue(\"*\", out _))\n                        {\n                            matchedDomain = \"*\";\n                            return true;\n                        }\n\n                        break;\n                    }\n\n                    domain = \"*.\" + parent;\n\n                    if (_domainMap.TryGetValue(domain, out _))\n                    {\n                        matchedDomain = domain;\n                        return true;\n                    }\n\n                    domain = parent;\n                }\n                while (true);\n\n                matchedDomain = null;\n                return false;\n            }\n\n            private bool TryGetForwarderRecords(string domain, out IReadOnlyList<DnsForwarderRecordData> forwarderRecords, out string matchedDomain)\n            {\n                if (IsDomainMatching(domain, out matchedDomain))\n                {\n                    forwarderRecords = _forwarderRecords;\n                    return true;\n                }\n\n                forwarderRecords = null;\n                return false;\n            }\n\n            #endregion\n\n            #region public\n\n            public void UpdateForwarderRecords(bool dnssecValidation, ConfigProxyServer proxy)\n            {\n                _forwarderRecords = GetUpdatedForwarderRecords(_forwarderRecords, dnssecValidation, proxy);\n            }\n\n            #endregion\n        }\n\n        class AdGuardUpstream : IDisposable\n        {\n            #region variables\n\n            static readonly char[] _popWordSeperator = new char[] { ' ' };\n\n            readonly IDnsServer _dnsServer;\n\n            readonly string _name;\n            ConfigProxyServer _configProxyServer;\n            bool _dnssecValidation;\n\n            List<DnsForwarderRecordData> _defaultForwarderRecords;\n            List<Forwarding> _forwardings;\n\n            readonly string _configFile;\n            DateTime _configFileLastModified;\n\n            Timer _autoReloadTimer;\n            const int AUTO_RELOAD_TIMER_INTERVAL = 60000;\n\n            #endregion\n\n            #region constructor\n\n            public AdGuardUpstream(IDnsServer dnsServer, Dictionary<string, ConfigProxyServer> configProxyServers, JsonElement jsonAdguardUpstream)\n            {\n                _dnsServer = dnsServer;\n\n                _name = jsonAdguardUpstream.GetProperty(\"configFile\").GetString();\n\n                _configFile = _name;\n\n                if (!Path.IsPathRooted(_configFile))\n                    _configFile = Path.Combine(_dnsServer.ApplicationFolder, _configFile);\n\n                _autoReloadTimer = new Timer(delegate (object state)\n                {\n                    try\n                    {\n                        DateTime configFileLastModified = File.GetLastWriteTimeUtc(_configFile);\n                        if (configFileLastModified > _configFileLastModified)\n                        {\n                            ReloadUpstreamsFile();\n\n                            //force GC collection to remove old cache data from memory quickly\n                            GC.Collect();\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.WriteLog(ex);\n                    }\n                    finally\n                    {\n                        _autoReloadTimer?.Change(AUTO_RELOAD_TIMER_INTERVAL, Timeout.Infinite);\n                    }\n                });\n\n                ReloadConfig(configProxyServers, jsonAdguardUpstream);\n            }\n\n            #endregion\n\n            #region IDisposable\n\n            public void Dispose()\n            {\n                if (_autoReloadTimer is not null)\n                {\n                    _autoReloadTimer.Dispose();\n                    _autoReloadTimer = null;\n                }\n            }\n\n            #endregion\n\n            #region private\n\n            private void ReloadUpstreamsFile()\n            {\n                try\n                {\n                    _dnsServer.WriteLog(\"The app is reading AdGuard Upstreams config file: \" + _configFile);\n\n                    List<DnsForwarderRecordData> defaultForwarderRecords = new List<DnsForwarderRecordData>();\n                    List<Forwarding> forwardings = new List<Forwarding>();\n\n                    using (FileStream fS = new FileStream(_configFile, FileMode.Open, FileAccess.Read))\n                    {\n                        StreamReader sR = new StreamReader(fS, true);\n                        string line;\n\n                        while (true)\n                        {\n                            line = sR.ReadLine();\n                            if (line is null)\n                                break; //eof\n\n                            line = line.TrimStart();\n\n                            if (line.Length == 0)\n                                continue; //skip empty line\n\n                            if (line.StartsWith('#'))\n                                continue; //skip comment line\n\n                            if (line.StartsWith('['))\n                            {\n                                int i = line.LastIndexOf(']');\n                                if (i < 0)\n                                    throw new FormatException(\"Invalid AdGuard Upstreams config file format: missing ']' bracket.\");\n\n                                string[] domains = line.Substring(1, i - 1).Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n                                string forwarder = line.Substring(i + 1);\n\n                                if (forwarder == \"#\")\n                                {\n                                    if (defaultForwarderRecords.Count == 0)\n                                        throw new FormatException(\"Invalid AdGuard Upstreams config file format: missing default upstream servers.\");\n\n                                    forwardings.Add(new Forwarding(defaultForwarderRecords, domains));\n                                }\n                                else\n                                {\n                                    List<DnsForwarderRecordData> forwarderRecords = new List<DnsForwarderRecordData>();\n                                    string word = PopWord(ref forwarder);\n\n                                    while (word.Length > 0)\n                                    {\n                                        string nextWord = PopWord(ref forwarder);\n\n                                        if (nextWord.StartsWith('('))\n                                        {\n                                            word += \" \" + nextWord;\n                                            nextWord = PopWord(ref forwarder);\n                                        }\n\n                                        forwarderRecords.Add(GetForwarderRecord(NameServerAddress.Parse(word), _dnssecValidation, _configProxyServer));\n\n                                        word = nextWord;\n                                    }\n\n                                    if (forwarderRecords.Count == 0)\n                                        throw new FormatException(\"Invalid AdGuard Upstreams config file format: missing upstream servers.\");\n\n                                    forwardings.Add(new Forwarding(forwarderRecords, domains));\n                                }\n                            }\n                            else\n                            {\n                                defaultForwarderRecords.Add(GetForwarderRecord(NameServerAddress.Parse(line), _dnssecValidation, _configProxyServer));\n                            }\n                        }\n\n                        _configFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle);\n                    }\n\n                    _defaultForwarderRecords = defaultForwarderRecords;\n                    _forwardings = forwardings;\n\n                    _dnsServer.WriteLog(\"The app has successfully loaded AdGuard Upstreams config file: \" + _configFile);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(\"The app failed to read AdGuard Upstreams config file: \" + _configFile + \"\\r\\n\" + ex.ToString());\n                }\n            }\n\n            private static string PopWord(ref string line)\n            {\n                if (line.Length == 0)\n                    return line;\n\n                line = line.TrimStart(_popWordSeperator);\n\n                int i = line.IndexOfAny(_popWordSeperator);\n                string word;\n\n                if (i < 0)\n                {\n                    word = line;\n                    line = \"\";\n                }\n                else\n                {\n                    word = line.Substring(0, i);\n                    line = line.Substring(i + 1);\n                }\n\n                return word;\n            }\n\n            #endregion\n\n            #region public\n\n            public void ReloadConfig(Dictionary<string, ConfigProxyServer> configProxyServers, JsonElement jsonAdguardUpstream)\n            {\n                string proxyName = jsonAdguardUpstream.GetPropertyValue(\"proxy\", null);\n                _dnssecValidation = jsonAdguardUpstream.GetPropertyValue(\"dnssecValidation\", true);\n\n                ConfigProxyServer configProxyServer = null;\n\n                if (!string.IsNullOrEmpty(proxyName) && ((configProxyServers is null) || !configProxyServers.TryGetValue(proxyName, out configProxyServer)))\n                    throw new FormatException(\"Proxy server was not defined: \" + proxyName);\n\n                _configProxyServer = configProxyServer;\n\n                DateTime configFileLastModified = File.GetLastWriteTimeUtc(_configFile);\n                if (configFileLastModified > _configFileLastModified)\n                {\n                    //reload complete config file\n                    _autoReloadTimer.Change(0, Timeout.Infinite);\n                }\n                else\n                {\n                    //update only forwarder records\n                    _defaultForwarderRecords = GetUpdatedForwarderRecords(_defaultForwarderRecords, _dnssecValidation, _configProxyServer);\n\n                    foreach (Forwarding forwarding in _forwardings)\n                        forwarding.UpdateForwarderRecords(_dnssecValidation, _configProxyServer);\n                }\n            }\n\n            public bool TryGetForwarderRecords(string domain, out IReadOnlyList<DnsForwarderRecordData> forwarderRecords)\n            {\n                if ((_forwardings is not null) && (_forwardings.Count > 0))\n                {\n                    if (Forwarding.IsForwarderDomain(domain, _forwardings))\n                    {\n                        forwarderRecords = null;\n                        return false;\n                    }\n\n                    if (Forwarding.TryGetForwarderRecords(domain, _forwardings, out forwarderRecords))\n                        return true;\n                }\n\n                if ((_defaultForwarderRecords is not null) && (_defaultForwarderRecords.Count > 0))\n                {\n                    if (Forwarding.IsForwarderDomain(domain, _defaultForwarderRecords))\n                    {\n                        forwarderRecords = null;\n                        return false;\n                    }\n\n                    forwarderRecords = _defaultForwarderRecords;\n                    return true;\n                }\n\n                forwarderRecords = null;\n                return false;\n            }\n\n            #endregion\n\n            #region property\n\n            public string Name\n            { get { return _name; } }\n\n            #endregion\n        }\n\n        class ConfigProxyServer\n        {\n            #region variables\n\n            readonly string _name;\n            readonly DnsForwarderRecordProxyType _type;\n            readonly string _proxyAddress;\n            readonly ushort _proxyPort;\n            readonly string _proxyUsername;\n            readonly string _proxyPassword;\n\n            #endregion\n\n            #region constructor\n\n            public ConfigProxyServer(JsonElement jsonProxy)\n            {\n                _name = jsonProxy.GetProperty(\"name\").GetString();\n                _type = jsonProxy.GetPropertyEnumValue(\"type\", DnsForwarderRecordProxyType.Http);\n                _proxyAddress = jsonProxy.GetProperty(\"proxyAddress\").GetString();\n                _proxyPort = jsonProxy.GetProperty(\"proxyPort\").GetUInt16();\n                _proxyUsername = jsonProxy.GetPropertyValue(\"proxyUsername\", null);\n                _proxyPassword = jsonProxy.GetPropertyValue(\"proxyPassword\", null);\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public DnsForwarderRecordProxyType Type\n            { get { return _type; } }\n\n            public string ProxyAddress\n            { get { return _proxyAddress; } }\n\n            public ushort ProxyPort\n            { get { return _proxyPort; } }\n\n            public string ProxyUsername\n            { get { return _proxyUsername; } }\n\n            public string ProxyPassword\n            { get { return _proxyPassword; } }\n\n            #endregion\n        }\n\n        class ConfigForwarder\n        {\n            #region variables\n\n            readonly string _name;\n            readonly DnsForwarderRecordData[] _forwarderRecords;\n\n            #endregion\n\n            #region constructor\n\n            public ConfigForwarder(JsonElement jsonForwarder, Dictionary<string, ConfigProxyServer> configProxyServers)\n            {\n                _name = jsonForwarder.GetProperty(\"name\").GetString();\n\n                string proxyName = jsonForwarder.GetPropertyValue(\"proxy\", null);\n                bool dnssecValidation = jsonForwarder.GetPropertyValue(\"dnssecValidation\", true);\n                DnsTransportProtocol forwarderProtocol = jsonForwarder.GetPropertyEnumValue(\"forwarderProtocol\", DnsTransportProtocol.Udp);\n\n                ConfigProxyServer configProxyServer = null;\n\n                if (!string.IsNullOrEmpty(proxyName) && ((configProxyServers is null) || !configProxyServers.TryGetValue(proxyName, out configProxyServer)))\n                    throw new FormatException(\"Proxy server was not defined: \" + proxyName);\n\n                _forwarderRecords = jsonForwarder.ReadArray(\"forwarderAddresses\", delegate (string address)\n                {\n                    return GetForwarderRecord(forwarderProtocol, address, dnssecValidation, configProxyServer);\n                });\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public DnsForwarderRecordData[] ForwarderRecords\n            { get { return _forwarderRecords; } }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/AdvancedForwardingApp/adguard-upstreams.txt",
    "content": "# AdGuard Upstreams\n# File Format Reference: https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams\n#\n# Example:\n# 8.8.8.8\n# udp://9.9.9.9\n# [/host.com/example.com/]https://cloudflare-dns.com/dns-query (1.1.1.1) tls://1.1.1.1\n# [/maps.host.com/]#\n# [/home/]192.168.10.2\n# [/test.com/]https://dns.quad9.net/dns-query (9.9.9.9)\n"
  },
  {
    "path": "Apps/AdvancedForwardingApp/dnsApp.config",
    "content": "{\n  \"appPreference\": 200,\n  \"enableForwarding\": true,\n  \"proxyServers\": [\n    {\n      \"name\": \"local-proxy\",\n      \"type\": \"socks5\",\n      \"proxyAddress\": \"localhost\",\n      \"proxyPort\": 1080,\n      \"proxyUsername\": null,\n      \"proxyPassword\": null\n    }\n  ],\n  \"forwarders\": [\n    {\n      \"name\": \"quad9-doh\",\n      \"proxy\": null,\n      \"dnssecValidation\": true,\n      \"forwarderProtocol\": \"Https\",\n      \"forwarderAddresses\": [\n        \"https://dns.quad9.net/dns-query (9.9.9.9)\"\n      ]\n    },\n    {\n      \"name\": \"cloudflare-google\",\n      \"proxy\": null,\n      \"dnssecValidation\": true,\n      \"forwarderProtocol\": \"Tls\",\n      \"forwarderAddresses\": [\n        \"1.1.1.1\",\n        \"8.8.8.8\"\n      ]\n    },\n    {\n      \"name\": \"quad9-tls-proxied\",\n      \"proxy\": \"local-proxy\",\n      \"dnssecValidation\": true,\n      \"forwarderProtocol\": \"Tls\",\n      \"forwarderAddresses\": [\n        \"9.9.9.9\"\n      ]\n    }\n  ],\n  \"networkGroupMap\": {\n    \"0.0.0.0/0\": \"everyone\",\n    \"[::]/0\": \"everyone\"\n  },\n  \"groups\": [\n    {\n      \"name\": \"everyone\",\n      \"enableForwarding\": true,\n      \"forwardings\": [\n        {\n          \"forwarders\": [\n            \"quad9-doh\"\n          ],\n          \"domains\": [\n            \"example.com\"\n          ]\n        },\n        {\n          \"forwarders\": [\n            \"cloudflare-google\"\n          ],\n          \"domains\": [\n            \"*\"\n          ]\n        }\n      ],\n      \"adguardUpstreams\": [\n        {\n          \"proxy\": null,\n          \"dnssecValidation\": true,\n          \"configFile\": \"adguard-upstreams.txt\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "Apps/AutoPtrApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace AutoPtr\n{\n    public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n            string qname = question.Name;\n\n            if (qname.Length == appRecordName.Length)\n                return null;\n\n            if (!IPAddressExtensions.TryParseReverseDomain(qname.ToLowerInvariant(), out IPAddress address))\n                return null;\n\n            if (question.Type != DnsResourceRecordType.PTR)\n            {\n                //NODATA reponse\n                DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN));\n\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer);\n            }\n\n            string domain = null;\n\n            using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))\n            {\n                JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n                string ipSeparator;\n\n                if (jsonAppRecordData.TryGetProperty(\"ipSeparator\", out JsonElement jsonSeparator) && (jsonSeparator.ValueKind != JsonValueKind.Null))\n                    ipSeparator = jsonSeparator.ToString();\n                else\n                    ipSeparator = string.Empty;\n\n                switch (address.AddressFamily)\n                {\n                    case AddressFamily.InterNetwork:\n                        {\n                            byte[] buffer = address.GetAddressBytes();\n\n                            foreach (byte b in buffer)\n                            {\n                                if (domain is null)\n                                    domain = b.ToString();\n                                else\n                                    domain += ipSeparator + b.ToString();\n                            }\n                        }\n                        break;\n\n                    case AddressFamily.InterNetworkV6:\n                        {\n                            byte[] buffer = address.GetAddressBytes();\n\n                            for (int i = 0; i < buffer.Length; i += 2)\n                            {\n                                if (domain is null)\n                                    domain = buffer[i].ToString(\"x2\") + buffer[i + 1].ToString(\"x2\");\n                                else\n                                    domain += ipSeparator + buffer[i].ToString(\"x2\") + buffer[i + 1].ToString(\"x2\");\n                            }\n                        }\n                        break;\n\n                    default:\n                        return null;\n                }\n\n                if (jsonAppRecordData.TryGetProperty(\"prefix\", out JsonElement jsonPrefix) && (jsonPrefix.ValueKind != JsonValueKind.Null))\n                    domain = jsonPrefix.GetString() + domain;\n\n                if (jsonAppRecordData.TryGetProperty(\"suffix\", out JsonElement jsonSuffix) && (jsonSuffix.ValueKind != JsonValueKind.Null))\n                    domain += jsonSuffix.GetString();\n            }\n\n            DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(qname, DnsResourceRecordType.PTR, DnsClass.IN, appRecordTtl, new DnsPTRRecordData(domain)) };\n\n            return new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Returns automatically generated response for a PTR request for both IPv4 and IPv6.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"prefix\"\": \"\"\"\",\n  \"\"suffix\"\": \"\".example.com\"\",\n  \"\"ipSeparator\"\": \"\"-\"\"\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/AutoPtrApp/AutoPtrApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>AutoPtrApp</AssemblyName>\n\t\t<RootNamespace>AutoPtr</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/AutoPtrApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/BlockPageApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.ResponseCompression;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.AspNetCore.StaticFiles;\nusing Microsoft.Extensions.FileProviders;\nusing Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Security;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace BlockPage\n{\n    public sealed class App : IDnsApplication\n    {\n        #region variables\n\n        IReadOnlyDictionary<string, WebServer> _webServers;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            StopAllWebServersAsync().Sync();\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task StopAllWebServersAsync()\n        {\n            if (_webServers is not null)\n            {\n                foreach (KeyValuePair<string, WebServer> webServerEntry in _webServers)\n                    await webServerEntry.Value.DisposeAsync();\n\n                _webServers = null;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            await StopAllWebServersAsync();\n\n            Dictionary<string, WebServer> webServers = new Dictionary<string, WebServer>(3);\n            _webServers = webServers;\n\n            if (jsonConfig.ValueKind == JsonValueKind.Array)\n            {\n                foreach (JsonElement jsonWebServerConfig in jsonConfig.EnumerateArray())\n                {\n                    string name = jsonWebServerConfig.GetPropertyValue(\"name\", \"default\");\n\n                    if (!webServers.TryGetValue(name, out WebServer webServer))\n                    {\n                        webServer = new WebServer(dnsServer, name);\n\n                        if (!webServers.TryAdd(webServer.Name, webServer))\n                            throw new InvalidOperationException(\"Failed to update web server config. Please try again.\");\n                    }\n\n                    await webServer.InitializeAsync(jsonWebServerConfig);\n                }\n            }\n            else\n            {\n                WebServer webServer = new WebServer(dnsServer, \"default\");\n                webServers.Add(webServer.Name, webServer);\n\n                await webServer.InitializeAsync(jsonConfig);\n\n                if (!jsonConfig.TryGetProperty(\"webServerUseSelfSignedTlsCertificate\", out _))\n                    config = config.Replace(\"\\\"webServerTlsCertificateFilePath\\\"\", \"\\\"webServerUseSelfSignedTlsCertificate\\\": true,\\r\\n  \\\"webServerTlsCertificateFilePath\\\"\");\n\n                if (!jsonConfig.TryGetProperty(\"enableWebServer\", out _))\n                    config = config.Replace(\"\\\"webServerLocalAddresses\\\"\", \"\\\"enableWebServer\\\": true,\\r\\n  \\\"webServerLocalAddresses\\\"\");\n\n                if (!jsonConfig.TryGetProperty(\"name\", out _))\n                    config = config.Replace(\"\\\"enableWebServer\\\"\", \"\\\"name\\\": \\\"default\\\",\\r\\n  \\\"enableWebServer\\\"\");\n\n                config = \"[\\r\\n  \" + config.Replace(\"\\n\", \"\\n  \").TrimEnd() + \"\\r\\n]\";\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        #endregion\n\n        class WebServer : IAsyncDisposable\n        {\n            #region variables\n\n            readonly IDnsServer _dnsServer;\n            readonly string _name;\n\n            IReadOnlyList<IPAddress> _webServerLocalAddresses = Array.Empty<IPAddress>();\n            bool _webServerUseSelfSignedTlsCertificate;\n            string _webServerTlsCertificateFilePath;\n            string _webServerTlsCertificatePassword;\n            string _webServerRootPath;\n            bool _serveBlockPageFromWebServerRoot;\n            bool _includeBlockingInfo;\n\n            string _blockPageContent;\n\n            WebApplication _webServer;\n\n            SslServerAuthenticationOptions _sslServerAuthenticationOptions;\n            DateTime _webServerTlsCertificateLastModifiedOn;\n\n            Timer _tlsCertificateUpdateTimer;\n            const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000;\n            const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000;\n\n            #endregion\n\n            #region constructor\n\n            public WebServer(IDnsServer dnsServer, string name)\n            {\n                _dnsServer = dnsServer;\n                _name = name;\n            }\n\n            #endregion\n\n            #region IDisposable\n\n            bool _disposed;\n\n            public async ValueTask DisposeAsync()\n            {\n                if (_disposed)\n                    return;\n\n                await StopTlsCertificateUpdateTimerAsync();\n                await StopWebServerAsync();\n\n                _disposed = true;\n            }\n\n            #endregion\n\n            #region private\n\n            private async Task StartWebServerAsync()\n            {\n                WebApplicationBuilder builder = WebApplication.CreateBuilder();\n\n                if (_serveBlockPageFromWebServerRoot)\n                {\n                    builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(_dnsServer.ApplicationFolder)\n                    {\n                        UseActivePolling = true,\n                        UsePollingFileWatcher = true\n                    };\n\n                    builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_webServerRootPath)\n                    {\n                        UseActivePolling = true,\n                        UsePollingFileWatcher = true\n                    };\n                }\n\n                builder.Services.AddResponseCompression(delegate (ResponseCompressionOptions options)\n                {\n                    options.EnableForHttps = true;\n                });\n\n                builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions)\n                {\n                    //http\n                    foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)\n                        serverOptions.Listen(webServiceLocalAddress, 80);\n\n                    //https\n                    if (_sslServerAuthenticationOptions is not null)\n                    {\n                        foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)\n                        {\n                            serverOptions.Listen(webServiceLocalAddress, 443, delegate (ListenOptions listenOptions)\n                            {\n                                listenOptions.Protocols = HttpProtocols.Http1AndHttp2;\n                                listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)\n                                {\n                                    return ValueTask.FromResult(_sslServerAuthenticationOptions);\n                                }, null);\n                            });\n                        }\n                    }\n\n                    serverOptions.AddServerHeader = false;\n                    serverOptions.Limits.MaxRequestBodySize = int.MaxValue;\n                });\n\n                builder.Logging.ClearProviders();\n\n                _webServer = builder.Build();\n\n                _webServer.UseResponseCompression();\n\n                _webServer.UseDefaultFiles();\n                _webServer.UseStaticFiles(new StaticFileOptions()\n                {\n                    OnPrepareResponse = delegate (StaticFileResponseContext ctx)\n                    {\n                        ctx.Context.Response.Headers[\"X-Robots-Tag\"] = \"noindex, nofollow\";\n                        ctx.Context.Response.Headers.CacheControl = \"no-cache\";\n                    },\n                    ServeUnknownFileTypes = true\n                });\n\n                if (_serveBlockPageFromWebServerRoot)\n                    _webServer.Use(RedirectToDefaultPageAsync);\n                else\n                    _webServer.Use(ServeDefaultPageAsync);\n\n                try\n                {\n                    await _webServer.StartAsync();\n\n                    foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)\n                    {\n                        _dnsServer.WriteLog(\"Web server '\" + _name + \"' was bound successfully: \" + new IPEndPoint(webServiceLocalAddress, 80).ToString());\n\n                        if (_sslServerAuthenticationOptions is not null)\n                            _dnsServer.WriteLog(\"Web server '\" + _name + \"' was bound successfully: \" + new IPEndPoint(webServiceLocalAddress, 443).ToString());\n                    }\n                }\n                catch (Exception ex)\n                {\n                    await StopWebServerAsync();\n\n                    foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses)\n                    {\n                        _dnsServer.WriteLog(\"Web server '\" + _name + \"' failed to bind: \" + new IPEndPoint(webServiceLocalAddress, 80).ToString());\n\n                        if (_sslServerAuthenticationOptions is not null)\n                            _dnsServer.WriteLog(\"Web server '\" + _name + \"' failed to bind: \" + new IPEndPoint(webServiceLocalAddress, 443).ToString());\n                    }\n\n                    _dnsServer.WriteLog(ex);\n                }\n            }\n\n            private async Task StopWebServerAsync()\n            {\n                if (_webServer is not null)\n                {\n                    await _webServer.DisposeAsync();\n                    _webServer = null;\n                }\n            }\n\n            private void LoadWebServiceTlsCertificate(string webServerTlsCertificateFilePath, string webServerTlsCertificatePassword)\n            {\n                FileInfo fileInfo = new FileInfo(webServerTlsCertificateFilePath);\n\n                if (!fileInfo.Exists)\n                    throw new ArgumentException(\"Web server '\" + _name + \"' TLS certificate file does not exists: \" + webServerTlsCertificateFilePath);\n\n                switch (Path.GetExtension(webServerTlsCertificateFilePath).ToLowerInvariant())\n                {\n                    case \".pfx\":\n                    case \".p12\":\n                        break;\n\n                    default:\n                        throw new ArgumentException(\"Web server '\" + _name + \"' TLS certificate file must be PKCS #12 formatted with .pfx or .p12 extension: \" + webServerTlsCertificateFilePath);\n                }\n\n                X509Certificate2Collection webServerTlsCertificateCollection = X509CertificateLoader.LoadPkcs12CollectionFromFile(webServerTlsCertificateFilePath, webServerTlsCertificatePassword, X509KeyStorageFlags.PersistKeySet);\n                X509Certificate2 serverCertificate = null;\n\n                foreach (X509Certificate2 certificate in webServerTlsCertificateCollection)\n                {\n                    if (certificate.HasPrivateKey)\n                    {\n                        serverCertificate = certificate;\n                        break;\n                    }\n                }\n\n                if (serverCertificate is null)\n                    throw new ArgumentException(\"Web server '\" + _name + \"' TLS certificate file must contain a certificate with private key.\");\n\n                _sslServerAuthenticationOptions = new SslServerAuthenticationOptions()\n                {\n                    ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, webServerTlsCertificateCollection, false)\n                };\n\n                _webServerTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc;\n\n                _dnsServer.WriteLog(\"Web server '\" + _name + \"' TLS certificate was loaded: \" + webServerTlsCertificateFilePath);\n            }\n\n            private void StartTlsCertificateUpdateTimer()\n            {\n                if (_tlsCertificateUpdateTimer is null)\n                {\n                    _tlsCertificateUpdateTimer = new Timer(delegate (object state)\n                    {\n                        if (!string.IsNullOrEmpty(_webServerTlsCertificateFilePath))\n                        {\n                            try\n                            {\n                                FileInfo fileInfo = new FileInfo(_webServerTlsCertificateFilePath);\n\n                                if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _webServerTlsCertificateLastModifiedOn))\n                                    LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword);\n                            }\n                            catch (Exception ex)\n                            {\n                                _dnsServer.WriteLog(\"Web server '\" + _name + \"' encountered an error while updating TLS Certificate: \" + _webServerTlsCertificateFilePath + \"\\r\\n\" + ex.ToString());\n                            }\n                        }\n\n                    }, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL);\n                }\n            }\n\n            private async Task StopTlsCertificateUpdateTimerAsync()\n            {\n                if (_tlsCertificateUpdateTimer is not null)\n                {\n                    await _tlsCertificateUpdateTimer.DisposeAsync();\n                    _tlsCertificateUpdateTimer = null;\n                }\n            }\n\n            private Task RedirectToDefaultPageAsync(HttpContext context, RequestDelegate next)\n            {\n                context.Response.Redirect(\"/\", false, true);\n\n                return Task.CompletedTask;\n            }\n\n            private async Task ServeDefaultPageAsync(HttpContext context, RequestDelegate next)\n            {\n                string blockPageContent = _blockPageContent;\n\n                if (_includeBlockingInfo)\n                {\n                    string blockingInfoHtmlContent = null;\n\n                    try\n                    {\n                        string host = context.Request.Host.Host;\n                        if (host is not null)\n                        {\n                            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);\n                            DnsDatagram dnsResponse = await _dnsServer.DirectQueryAsync(dnsRequest, 500);\n\n                            List<EDnsExtendedDnsErrorOptionData> options = new List<EDnsExtendedDnsErrorOptionData>();\n\n                            if (dnsResponse.EDNS is not null)\n                            {\n                                foreach (EDnsOption option in dnsResponse.EDNS.Options)\n                                {\n                                    if (option.Code == EDnsOptionCode.EXTENDED_DNS_ERROR)\n                                    {\n                                        EDnsExtendedDnsErrorOptionData ede = option.Data as EDnsExtendedDnsErrorOptionData;\n                                        options.Add(ede);\n                                    }\n                                }\n                            }\n\n                            options.AddRange(dnsResponse.DnsClientExtendedErrors);\n\n                            foreach (EDnsExtendedDnsErrorOptionData option in options)\n                            {\n                                if (blockingInfoHtmlContent is null)\n                                    blockingInfoHtmlContent = \"  <p><b>Detailed Info</b><br>\" + option.InfoCode.ToString() + (option.ExtraText is null ? \"\" : \": \" + option.ExtraText);\n                                else\n                                    blockingInfoHtmlContent += \"<br>\" + option.InfoCode.ToString() + (option.ExtraText is null ? \"\" : \": \" + option.ExtraText);\n                            }\n\n                            if (blockingInfoHtmlContent is not null)\n                                blockingInfoHtmlContent += \"</p>\";\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.WriteLog(ex);\n                    }\n\n                    if (blockingInfoHtmlContent is null)\n                        blockPageContent = blockPageContent.Replace(\"{BLOCKING-INFO}\", \"\");\n                    else\n                        blockPageContent = blockPageContent.Replace(\"{BLOCKING-INFO}\", blockingInfoHtmlContent);\n                }\n\n                byte[] finalBlockPageContent = Encoding.UTF8.GetBytes(blockPageContent);\n\n                HttpResponse response = context.Response;\n\n                response.StatusCode = StatusCodes.Status200OK;\n                response.ContentType = \"text/html; charset=utf-8\";\n                response.ContentLength = finalBlockPageContent.Length;\n\n                using (Stream s = context.Response.Body)\n                {\n                    await s.WriteAsync(finalBlockPageContent);\n                }\n            }\n\n            #endregion\n\n            #region public\n\n            public async Task InitializeAsync(JsonElement jsonWebServerConfig)\n            {\n                bool enableWebServer = jsonWebServerConfig.GetPropertyValue(\"enableWebServer\", true);\n                if (!enableWebServer)\n                {\n                    await StopWebServerAsync();\n                    return;\n                }\n\n                _webServerLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(jsonWebServerConfig.ReadArray(\"webServerLocalAddresses\", IPAddress.Parse));\n\n                if (jsonWebServerConfig.TryGetProperty(\"webServerUseSelfSignedTlsCertificate\", out JsonElement jsonWebServerUseSelfSignedTlsCertificate))\n                    _webServerUseSelfSignedTlsCertificate = jsonWebServerUseSelfSignedTlsCertificate.GetBoolean();\n                else\n                    _webServerUseSelfSignedTlsCertificate = true;\n\n                _webServerTlsCertificateFilePath = jsonWebServerConfig.GetProperty(\"webServerTlsCertificateFilePath\").GetString();\n                _webServerTlsCertificatePassword = jsonWebServerConfig.GetProperty(\"webServerTlsCertificatePassword\").GetString();\n\n                _webServerRootPath = jsonWebServerConfig.GetProperty(\"webServerRootPath\").GetString();\n\n                if (!Path.IsPathRooted(_webServerRootPath))\n                    _webServerRootPath = Path.Combine(_dnsServer.ApplicationFolder, _webServerRootPath);\n\n                _serveBlockPageFromWebServerRoot = jsonWebServerConfig.GetProperty(\"serveBlockPageFromWebServerRoot\").GetBoolean();\n\n                string blockPageTitle = jsonWebServerConfig.GetProperty(\"blockPageTitle\").GetString();\n                string blockPageHeading = jsonWebServerConfig.GetProperty(\"blockPageHeading\").GetString();\n                string blockPageMessage = jsonWebServerConfig.GetProperty(\"blockPageMessage\").GetString();\n\n                _includeBlockingInfo = jsonWebServerConfig.GetPropertyValue(\"includeBlockingInfo\", true);\n\n                _blockPageContent = @\"<html>\n<head>\n  <title>\" + (blockPageTitle is null ? \"\" : blockPageTitle) + @\"</title>\n</head>\n<body>\n\" + (blockPageHeading is null ? \"\" : \"  <h1>\" + blockPageHeading + \"</h1>\") + @\"\n\" + (blockPageMessage is null ? \"\" : \"  <p>\" + blockPageMessage + \"</p>\") + @\"\n\" + (_includeBlockingInfo ? \"{BLOCKING-INFO}\" : \"\") + @\"\n</body>\n</html>\";\n\n                try\n                {\n                    await StopWebServerAsync();\n\n                    string selfSignedCertificateFilePath = Path.Combine(_dnsServer.ApplicationFolder, \"self-signed-cert.pfx\");\n\n                    if (_webServerUseSelfSignedTlsCertificate)\n                    {\n                        string oldSelfSignedCertificateFilePath = Path.Combine(_dnsServer.ApplicationFolder, \"cert.pfx\");\n\n                        if (!oldSelfSignedCertificateFilePath.Equals(_webServerTlsCertificateFilePath, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) && File.Exists(oldSelfSignedCertificateFilePath) && !File.Exists(selfSignedCertificateFilePath))\n                            File.Move(oldSelfSignedCertificateFilePath, selfSignedCertificateFilePath);\n\n                        if (!File.Exists(selfSignedCertificateFilePath))\n                        {\n                            RSA rsa = RSA.Create(2048);\n                            CertificateRequest req = new CertificateRequest(\"cn=\" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n                            X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5));\n\n                            await File.WriteAllBytesAsync(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string));\n                        }\n                    }\n                    else\n                    {\n                        File.Delete(selfSignedCertificateFilePath);\n                    }\n\n                    if (string.IsNullOrEmpty(_webServerTlsCertificateFilePath))\n                    {\n                        await StopTlsCertificateUpdateTimerAsync();\n\n                        if (_webServerUseSelfSignedTlsCertificate)\n                        {\n                            LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null);\n                        }\n                        else\n                        {\n                            //disable HTTPS\n                            _sslServerAuthenticationOptions = null;\n                        }\n                    }\n                    else\n                    {\n                        LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword);\n                        StartTlsCertificateUpdateTimer();\n                    }\n\n                    await StartWebServerAsync();\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(ex);\n                }\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/BlockPageApp/BlockPageApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>7.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>BlockPageApp</AssemblyName>\n\t\t<RootNamespace>BlockPage</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Remove=\"wwwroot\\index.html\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Content Include=\"wwwroot\\index.html\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/BlockPageApp/dnsApp.config",
    "content": "[\n  {\n    \"name\": \"default\",\n    \"enableWebServer\": true,\n    \"webServerLocalAddresses\": [\n      \"0.0.0.0\",\n      \"::\"\n    ],\n    \"webServerUseSelfSignedTlsCertificate\": true,\n    \"webServerTlsCertificateFilePath\": null,\n    \"webServerTlsCertificatePassword\": null,\n    \"webServerRootPath\": \"wwwroot\",\n    \"serveBlockPageFromWebServerRoot\": false,\n    \"blockPageTitle\": \"Website Blocked\",\n    \"blockPageHeading\": \"Website Blocked\",\n    \"blockPageMessage\": \"This website has been blocked by your network administrator.\",\n    \"includeBlockingInfo\": true\n  }\n]\n"
  },
  {
    "path": "Apps/BlockPageApp/wwwroot/index.html",
    "content": "<html>\n<head>\n    <title>Website Blocked</title>\n</head>\n<body>\n    <h1>Website Blocked</h1>\n    <p>This website has been blocked by your network administrator.</p>\n</body>\n</html>"
  },
  {
    "path": "Apps/DefaultRecordsApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DefaultRecords\n{\n    public sealed class App : IDnsApplication, IDnsPostProcessor\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n\n        bool _enableDefaultRecords;\n        uint _defaultTtl;\n        Dictionary<string, string[]> _zoneSetMap;\n        Dictionary<string, Set> _sets;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region private\n\n        private static string GetParentZone(string domain)\n        {\n            int i = domain.IndexOf('.');\n            if (i > -1)\n                return domain.Substring(i + 1);\n\n            //dont return root zone\n            return null;\n        }\n\n        private bool TryGetMappedSets(string domain, out string zone, out string[] setNames)\n        {\n            domain = domain.ToLowerInvariant();\n\n            string parent;\n\n            do\n            {\n                if (_zoneSetMap.TryGetValue(domain, out setNames))\n                {\n                    zone = domain;\n                    return true;\n                }\n\n                parent = GetParentZone(domain);\n                if (parent is null)\n                {\n                    if (_zoneSetMap.TryGetValue(\"*\", out setNames))\n                    {\n                        zone = \"*\";\n                        return true;\n                    }\n\n                    break;\n                }\n\n                domain = \"*.\" + parent;\n\n                if (_zoneSetMap.TryGetValue(domain, out setNames))\n                {\n                    zone = domain;\n                    return true;\n                }\n\n                domain = parent;\n            }\n            while (true);\n\n            zone = null;\n            return false;\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _enableDefaultRecords = jsonConfig.GetProperty(\"enableDefaultRecords\").GetBoolean();\n            _defaultTtl = jsonConfig.GetPropertyValue(\"defaultTtl\", 3600u);\n\n            _zoneSetMap = jsonConfig.ReadObjectAsMap(\"zoneSetMap\", delegate (string zone, JsonElement jsonSets)\n            {\n                string[] sets = jsonSets.GetArray();\n\n                return new Tuple<string, string[]>(zone.ToLowerInvariant(), sets);\n            });\n\n            _sets = jsonConfig.ReadArrayAsMap(\"sets\", delegate (JsonElement jsonSet)\n            {\n                Set set = new Set(jsonSet);\n\n                return new Tuple<string, Set>(set.Name, set);\n            });\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (!_enableDefaultRecords)\n                return response;\n\n            if (!response.AuthoritativeAnswer || (response.OPCODE != DnsOpcode.StandardQuery))\n                return response;\n\n            switch (response.RCODE)\n            {\n                case DnsResponseCode.NoError:\n                case DnsResponseCode.NxDomain:\n                    break;\n\n                default:\n                    return response;\n            }\n\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!TryGetMappedSets(question.Name, out string zone, out string[] setNames))\n                return response;\n\n            if (zone.StartsWith('*'))\n            {\n                DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(question.Name, DnsResourceRecordType.SOA, DnsClass.IN));\n                if (soaResponse is null)\n                    return response;\n\n                if ((soaResponse.Answer.Count > 0) && (soaResponse.Answer[soaResponse.Answer.Count - 1].Type == DnsResourceRecordType.SOA))\n                    zone = soaResponse.Answer[soaResponse.Answer.Count - 1].Name;\n                else if ((soaResponse.Authority.Count > 0) && (soaResponse.Authority[0].Type == DnsResourceRecordType.SOA))\n                    zone = soaResponse.Authority[0].Name;\n                else\n                    return response;\n            }\n\n            StringBuilder sb = new StringBuilder();\n\n            foreach (string setName in setNames)\n            {\n                if (_sets.TryGetValue(setName, out Set set) && set.Enable)\n                {\n                    foreach (string record in set.Records)\n                        sb.AppendLine(record);\n                }\n            }\n\n            if (sb.Length == 0)\n                return response;\n\n            StringReader sR = new StringReader(sb.ToString());\n            List<DnsResourceRecord> records = ZoneFile.ReadZoneFileFromAsync(sR, zone, _defaultTtl).Sync();\n\n            List<DnsResourceRecord> newAnswer = new List<DnsResourceRecord>(response.Answer.Count + records.Count);\n            string qname = question.Name;\n\n            if (response.Answer.Count > 0)\n            {\n                newAnswer.AddRange(response.Answer);\n\n                DnsResourceRecord lastRR = response.Answer[response.Answer.Count - 1];\n                if (lastRR.Type == DnsResourceRecordType.CNAME)\n                    qname = (lastRR.RDATA as DnsCNAMERecordData).Domain;\n            }\n\n            foreach (DnsResourceRecord record in records)\n            {\n                if (record.Class != question.Class)\n                    continue;\n\n                if ((record.Type != question.Type) && (record.Type != DnsResourceRecordType.CNAME))\n                    continue;\n\n                if (!record.Name.Equals(qname, StringComparison.OrdinalIgnoreCase))\n                    continue;\n\n                newAnswer.Add(record);\n\n                if (record.Type == DnsResourceRecordType.CNAME)\n                    qname = (record.RDATA as DnsCNAMERecordData).Domain;\n            }\n\n            if (newAnswer.Count == response.Answer.Count)\n                return response;\n\n            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 };\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Enables default records for configured local zones.\"; } }\n\n        #endregion\n\n        class Set\n        {\n            #region variables\n\n            readonly string _name;\n            readonly bool _enable;\n            readonly string[] _records;\n\n            #endregion\n\n            #region constructor\n\n            public Set(JsonElement jsonSet)\n            {\n                _name = jsonSet.GetProperty(\"name\").GetString();\n                _enable = jsonSet.GetProperty(\"enable\").GetBoolean();\n                _records = jsonSet.ReadArray(\"records\");\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public bool Enable\n            { get { return _enable; } }\n\n            public string[] Records\n            { get { return _records; } }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/DefaultRecordsApp/DefaultRecordsApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    <Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>DefaultRecordsApp</AssemblyName>\n    <RootNamespace>DefaultRecords</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <Description>Allows setting one or more default records for configured local zones.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/DefaultRecordsApp/dnsApp.config",
    "content": "{\n  \"enableDefaultRecords\": false,\n  \"defaultTtl\": 3600,\n  \"zoneSetMap\": {\n    \"*\": [\"set1\"],\n    \"*.net\": [\"set2\"],\n    \"example.org\": [\"set1\", \"set2\"]\n  },\n  \"sets\": [\n    {\n      \"name\": \"set1\",\n      \"enable\": true,\n      \"records\": [\n        \"@ 3600 IN MX 10 mail.example.com.\",\n        \"@ 3600 IN TXT \\\"v=spf1 a mx -all\\\"\"\n      ]\n    },\n    {\n      \"name\": \"set2\",\n      \"enable\": true,\n      \"records\": [\n        \"www 3600 IN CNAME @\",\n        \"@ 3600 IN A 1.2.3.4\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "Apps/Dns64App/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace Dns64\n{\n    // DNS64: DNS Extensions for Network Address Translation from IPv6 Clients to IPv4 Servers\n    // https://www.rfc-editor.org/rfc/rfc6147\n\n    public sealed class App : IDnsApplication, IDnsPostProcessor, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n\n        byte _appPreference;\n\n        bool _enableDns64;\n        Dictionary<NetworkAddress, string> _networkGroupMap;\n        Dictionary<string, Group> _groups;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue(\"appPreference\", 30));\n\n            _enableDns64 = jsonConfig.GetProperty(\"enableDns64\").GetBoolean();\n\n            _networkGroupMap = jsonConfig.ReadObjectAsMap(\"networkGroupMap\", delegate (string network, JsonElement group)\n            {\n                if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress))\n                    throw new InvalidOperationException(\"Network group map contains an invalid network address: \" + network);\n\n                return new Tuple<NetworkAddress, string>(networkAddress, group.GetString());\n            });\n\n            _groups = jsonConfig.ReadArrayAsMap(\"groups\", delegate (JsonElement jsonGroup)\n            {\n                Group group = new Group(jsonGroup);\n                return new Tuple<string, Group>(group.Name, group);\n            });\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (!_enableDns64)\n                return response;\n\n            if (request.DnssecOk)\n                return response;\n\n            switch (response.RCODE)\n            {\n                case DnsResponseCode.NxDomain:\n                    return response;\n            }\n\n            DnsQuestionRecord question = request.Question[0];\n            if (question.Type != DnsResourceRecordType.AAAA)\n                return response;\n\n            IPAddress remoteIP = remoteEP.Address;\n            NetworkAddress network = null;\n            string groupName = null;\n\n            foreach (KeyValuePair<NetworkAddress, string> entry in _networkGroupMap)\n            {\n                if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength)))\n                {\n                    network = entry.Key;\n                    groupName = entry.Value;\n                }\n            }\n\n            if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableDns64)\n                return response;\n\n            List<DnsResourceRecord> newAnswer = new List<DnsResourceRecord>(response.Answer.Count);\n\n            bool synthesizeAAAA = true;\n\n            if (group.ExcludedIpv6.Length == 0)\n            {\n                //no exclusions configured\n                foreach (DnsResourceRecord answer in response.Answer)\n                {\n                    newAnswer.Add(answer);\n\n                    if (answer.Type == DnsResourceRecordType.AAAA)\n                        synthesizeAAAA = false; //found an AAAA record so no need to synthesize AAAA\n                }\n            }\n            else\n            {\n                //check for exclusions\n                foreach (DnsResourceRecord answer in response.Answer)\n                {\n                    if (answer.Type != DnsResourceRecordType.AAAA)\n                    {\n                        //keep non-AAAA record, most probably a CNAME record, in answer list\n                        newAnswer.Add(answer);\n                        continue;\n                    }\n\n                    IPAddress ipv6Address = (answer.RDATA as DnsAAAARecordData).Address;\n\n                    foreach (NetworkAddress excludedIpv6 in group.ExcludedIpv6)\n                    {\n                        if (!excludedIpv6.Contains(ipv6Address))\n                        {\n                            //found non-excluded AAAA record so no need to synthesize AAAA\n                            newAnswer.Add(answer);\n                            synthesizeAAAA = false;\n                        }\n                    }\n                }\n            }\n\n            if (!synthesizeAAAA)\n                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 };\n\n            DnsDatagram newResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN), 2000);\n\n            uint soaTtl;\n            {\n                DnsResourceRecord soa = response.FindFirstAuthorityRecord();\n                if ((soa is not null) && (soa.Type == DnsResourceRecordType.SOA))\n                    soaTtl = soa.TTL;\n                else\n                    soaTtl = 600;\n            }\n\n            foreach (DnsResourceRecord answer in newResponse.Answer)\n            {\n                if (answer.Type != DnsResourceRecordType.A)\n                    continue;\n\n                IPAddress ipv4Address = (answer.RDATA as DnsARecordData).Address;\n                NetworkAddress ipv4Network = null;\n                NetworkAddress dns64Prefix = null;\n\n                foreach (KeyValuePair<NetworkAddress, NetworkAddress> dns64PrefixEntry in group.Dns64PrefixMap)\n                {\n                    if (dns64PrefixEntry.Key.Contains(ipv4Address) && ((ipv4Network is null) || (dns64PrefixEntry.Key.PrefixLength > ipv4Network.PrefixLength)))\n                    {\n                        ipv4Network = dns64PrefixEntry.Key;\n                        dns64Prefix = dns64PrefixEntry.Value;\n                    }\n                }\n\n                if (dns64Prefix is null)\n                    continue;\n\n                IPAddress ipv6Address = ipv4Address.MapToIPv6(dns64Prefix);\n\n                newAnswer.Add(new DnsResourceRecord(answer.Name, DnsResourceRecordType.AAAA, answer.Class, Math.Min(answer.TTL, soaTtl), new DnsAAAARecordData(ipv6Address)));\n            }\n\n            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 };\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)\n        {\n            if (!_enableDns64)\n                return Task.FromResult<DnsDatagram>(null);\n\n            if (request.DnssecOk)\n                return Task.FromResult<DnsDatagram>(null);\n\n            DnsQuestionRecord question = request.Question[0];\n            if ((question.Type != DnsResourceRecordType.PTR) || !question.Name.EndsWith(\".ip6.arpa\", StringComparison.OrdinalIgnoreCase))\n                return Task.FromResult<DnsDatagram>(null);\n\n            IPAddress remoteIP = remoteEP.Address;\n            NetworkAddress network = null;\n            string groupName = null;\n\n            foreach (KeyValuePair<NetworkAddress, string> entry in _networkGroupMap)\n            {\n                if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength)))\n                {\n                    network = entry.Key;\n                    groupName = entry.Value;\n                }\n            }\n\n            if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableDns64)\n                return Task.FromResult<DnsDatagram>(null);\n\n            IPAddress ipv6Address = IPAddressExtensions.ParseReverseDomain(question.Name);\n            if (ipv6Address.AddressFamily != AddressFamily.InterNetworkV6)\n                return Task.FromResult<DnsDatagram>(null);\n\n            NetworkAddress dns64Prefix = null;\n\n            foreach (KeyValuePair<NetworkAddress, NetworkAddress> dns64PrefixEntry in group.Dns64PrefixMap)\n            {\n                if ((dns64PrefixEntry.Value is not null) && dns64PrefixEntry.Value.Contains(ipv6Address))\n                {\n                    dns64Prefix = dns64PrefixEntry.Value;\n                    break;\n                }\n            }\n\n            if (dns64Prefix is null)\n                return Task.FromResult<DnsDatagram>(null);\n\n            IPAddress ipv4Address = ipv6Address.MapToIPv4(dns64Prefix.PrefixLength);\n            IReadOnlyList<DnsResourceRecord> answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, 600, new DnsCNAMERecordData(ipv4Address.GetReverseDomain())) };\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Enables DNS64 function for both authoritative and recursive resolver responses for use by IPv6 only clients.\"; } }\n\n        public byte Preference\n        { get { return _appPreference; } }\n\n        #endregion\n\n        class Group\n        {\n            #region variables\n\n            readonly string _name;\n            readonly bool _enableDns64;\n            readonly Dictionary<NetworkAddress, NetworkAddress> _dns64PrefixMap;\n            readonly NetworkAddress[] _excludedIpv6;\n\n            #endregion\n\n            #region constructor\n\n            public Group(JsonElement jsonGroup)\n            {\n                _name = jsonGroup.GetProperty(\"name\").GetString();\n                _enableDns64 = jsonGroup.GetProperty(\"enableDns64\").GetBoolean();\n\n                _dns64PrefixMap = jsonGroup.ReadObjectAsMap(\"dns64PrefixMap\", delegate (string strNetwork, JsonElement jsonDns64Prefix)\n                {\n                    string strDns64Prefix = jsonDns64Prefix.GetString();\n\n                    NetworkAddress network = NetworkAddress.Parse(strNetwork);\n                    NetworkAddress dns64Prefix = null;\n\n                    if (strDns64Prefix is not null)\n                    {\n                        dns64Prefix = NetworkAddress.Parse(strDns64Prefix);\n\n                        switch (dns64Prefix.PrefixLength)\n                        {\n                            case 32:\n                            case 40:\n                            case 48:\n                            case 56:\n                            case 64:\n                            case 96:\n                                break;\n\n                            default:\n                                throw new NotSupportedException(\"DNS64 prefix can have only the following prefixes: 32, 40, 48, 56, 64, or 96.\");\n                        }\n                    }\n\n                    return new Tuple<NetworkAddress, NetworkAddress>(network, dns64Prefix);\n                });\n\n                _excludedIpv6 = jsonGroup.ReadArray(\"excludedIpv6\", delegate (string strNetworkAddress)\n                {\n                    NetworkAddress networkAddress = NetworkAddress.Parse(strNetworkAddress);\n                    if (networkAddress.Address.AddressFamily != AddressFamily.InterNetworkV6)\n                        throw new InvalidOperationException(\"An IPv6 network address is expected for 'excludedIpv6' array.\");\n\n                    return networkAddress;\n                });\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public bool EnableDns64\n            { get { return _enableDns64; } }\n\n            public Dictionary<NetworkAddress, NetworkAddress> Dns64PrefixMap\n            { get { return _dns64PrefixMap; } }\n\n            public NetworkAddress[] ExcludedIpv6\n            { get { return _excludedIpv6; } }\n\n            #endregion\n        }\n    }\n}"
  },
  {
    "path": "Apps/Dns64App/Dns64App.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>5.0</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>Dns64App</AssemblyName>\n\t\t<RootNamespace>Dns64</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/Dns64App/dnsApp.config",
    "content": "{\n  \"appPreference\": 30,\n  \"enableDns64\": true,\n  \"networkGroupMap\": {\n    \"::/0\": \"everyone\"\n  },\n  \"groups\": [\n    {\n      \"name\": \"everyone\",\n      \"enableDns64\": true,\n      \"dns64PrefixMap\": {\n        \"0.0.0.0/0\": \"64:ff9b::/96\",\n        \"10.0.0.0/8\": null,\n        \"172.16.0.0/12\": null,\n        \"192.168.0.0/16\": null\n      },\n      \"excludedIpv6\": [\n        \"::ffff:0:0/96\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "Apps/DnsBlockListApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsBlockList\n{\n    //DNS Blacklists and Whitelists\n    //https://www.rfc-editor.org/rfc/rfc5782\n\n    public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n\n        Dictionary<string, BlockList> _dnsBlockLists;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            if (_dnsBlockLists is not null)\n            {\n                foreach (KeyValuePair<string, BlockList> dnsBlockList in _dnsBlockLists)\n                    dnsBlockList.Value.Dispose();\n\n                _dnsBlockLists = null;\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private static bool TryParseDnsblDomain(string qName, string appRecordName, out IPAddress address, out string domain)\n        {\n            qName = qName.Substring(0, qName.Length - appRecordName.Length - 1);\n\n            string[] parts = qName.Split('.');\n            string lastPart = parts[parts.Length - 1];\n\n            if (byte.TryParse(lastPart, out _) || byte.TryParse(lastPart, NumberStyles.HexNumber, null, out _))\n            {\n                switch (parts.Length)\n                {\n                    case 4:\n                        {\n                            Span<byte> buffer = stackalloc byte[4];\n\n                            for (int i = 0, j = parts.Length - 1; (i < 4) && (j > -1); i++, j--)\n                                buffer[i] = byte.Parse(parts[j]);\n\n                            address = new IPAddress(buffer);\n                            domain = null;\n                            return true;\n                        }\n\n                    case 32:\n                        {\n                            Span<byte> buffer = stackalloc byte[16];\n\n                            for (int i = 0, j = parts.Length - 1; (i < 16) && (j > 0); i++, j -= 2)\n                                buffer[i] = (byte)(byte.Parse(parts[j], NumberStyles.HexNumber) << 4 | byte.Parse(parts[j - 1], NumberStyles.HexNumber));\n\n                            address = new IPAddress(buffer);\n                            domain = null;\n                            return true;\n                        }\n\n                    default:\n                        address = null;\n                        domain = null;\n                        return false;\n                }\n            }\n            else\n            {\n                address = null;\n                domain = lastPart;\n\n                for (int i = parts.Length - 2; i > -1; i--)\n                    domain = parts[i] + \".\" + domain;\n\n                return true;\n            }\n        }\n\n        private Tuple<string, BlockList> ReadBlockList(JsonElement jsonBlockList)\n        {\n            BlockList blockList;\n            string name = jsonBlockList.GetProperty(\"name\").GetString();\n            BlockListType type = jsonBlockList.GetPropertyEnumValue(\"type\", BlockListType.Ip);\n\n            if ((_dnsBlockLists is not null) && _dnsBlockLists.TryGetValue(name, out BlockList existingBlockList) && (existingBlockList.Type == type))\n            {\n                existingBlockList.ReloadConfig(jsonBlockList);\n                blockList = existingBlockList;\n            }\n            else\n            {\n                switch (type)\n                {\n                    case BlockListType.Ip:\n                        blockList = new IpBlockList(_dnsServer, jsonBlockList);\n                        break;\n\n                    case BlockListType.Domain:\n                        blockList = new DomainBlockList(_dnsServer, jsonBlockList);\n                        break;\n\n                    default:\n                        throw new NotSupportedException(\"DNSBL block list type is not supported: \" + type.ToString());\n                }\n            }\n\n            return new Tuple<string, BlockList>(blockList.Name, blockList);\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            if (jsonConfig.TryReadArrayAsMap(\"dnsBlockLists\", ReadBlockList, out Dictionary<string, BlockList> dnsBlockLists))\n            {\n                if (_dnsBlockLists is not null)\n                {\n                    foreach (KeyValuePair<string, BlockList> dnsBlockList in _dnsBlockLists)\n                    {\n                        if (!dnsBlockLists.ContainsKey(dnsBlockList.Key))\n                            dnsBlockList.Value.Dispose();\n                    }\n                }\n\n                _dnsBlockLists = dnsBlockLists;\n            }\n            else\n            {\n                if (_dnsBlockLists is not null)\n                {\n                    foreach (KeyValuePair<string, BlockList> dnsBlockList in _dnsBlockLists)\n                        dnsBlockList.Value.Dispose();\n                }\n\n                _dnsBlockLists = null;\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n            string qname = question.Name;\n\n            if (qname.Length == appRecordName.Length)\n                return null;\n\n            if ((_dnsBlockLists is null) || !TryParseDnsblDomain(qname, appRecordName, out IPAddress address, out string domain))\n                return null;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n            JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n            if (jsonAppRecordData.TryReadArray(\"dnsBlockLists\", out string[] dnsBlockLists))\n            {\n                bool isBlocked = false;\n                IPAddress responseA = null;\n                string responseTXT = null;\n\n                if (address is not null)\n                {\n                    foreach (string dnsBlockList in dnsBlockLists)\n                    {\n                        if (_dnsBlockLists.TryGetValue(dnsBlockList, out BlockList blockList) && blockList.Enabled && (blockList.Type == BlockListType.Ip) && blockList.IsBlocked(address, out responseA, out responseTXT))\n                        {\n                            isBlocked = true;\n\n                            if (!string.IsNullOrEmpty(responseTXT))\n                                responseTXT = responseTXT.Replace(\"{ip}\", address.ToString());\n\n                            break;\n                        }\n                    }\n                }\n                else if (domain is not null)\n                {\n                    foreach (string dnsBlockList in dnsBlockLists)\n                    {\n                        if (_dnsBlockLists.TryGetValue(dnsBlockList, out BlockList blockList) && blockList.Enabled && (blockList.Type == BlockListType.Domain) && blockList.IsBlocked(domain, out string foundDomain, out responseA, out responseTXT))\n                        {\n                            isBlocked = true;\n\n                            if (!string.IsNullOrEmpty(responseTXT))\n                                responseTXT = responseTXT.Replace(\"{domain}\", foundDomain);\n\n                            break;\n                        }\n                    }\n                }\n\n                if (isBlocked)\n                {\n                    switch (question.Type)\n                    {\n                        case DnsResourceRecordType.A:\n                            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)) });\n\n                        case DnsResourceRecordType.TXT:\n                            if (!string.IsNullOrEmpty(responseTXT))\n                                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)) });\n\n                            break;\n                    }\n\n                    //NODATA response\n                    DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN));\n\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer);\n                }\n            }\n\n            return null;\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"dnsBlockLists\"\": [\n    \"\"ipblocklist1\"\",\n    \"\"domainblocklist1\"\"\n  ]\n}\";\n            }\n        }\n\n        #endregion\n\n        enum BlockListType\n        {\n            Ip = 1,\n            Domain = 2\n        }\n\n        abstract class BlockList : IDisposable\n        {\n            #region variables\n\n            protected static readonly char[] _popWordSeperator = new char[] { ' ', '\\t', '|' };\n\n            protected readonly IDnsServer _dnsServer;\n            readonly BlockListType _type;\n\n            readonly string _name;\n            bool _enabled;\n            protected IPAddress _responseA;\n            protected string _responseTXT;\n            protected string _blockListFile;\n\n            protected DateTime _blockListFileLastModified;\n\n            Timer _autoReloadTimer;\n            const int AUTO_RELOAD_TIMER_INTERVAL = 60000;\n\n            #endregion\n\n            #region constructor\n\n            protected BlockList(IDnsServer dnsServer, BlockListType type, JsonElement jsonBlockList)\n            {\n                _dnsServer = dnsServer;\n                _type = type;\n\n                _name = jsonBlockList.GetProperty(\"name\").GetString();\n\n                _autoReloadTimer = new Timer(delegate (object state)\n                {\n                    try\n                    {\n                        DateTime blockListFileLastModified = File.GetLastWriteTimeUtc(_blockListFile);\n                        if (blockListFileLastModified > _blockListFileLastModified)\n                            ReloadBlockListFile();\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.WriteLog(ex);\n                    }\n                    finally\n                    {\n                        _autoReloadTimer?.Change(AUTO_RELOAD_TIMER_INTERVAL, Timeout.Infinite);\n                    }\n                });\n\n                ReloadConfig(jsonBlockList);\n            }\n\n            #endregion\n\n            #region IDisposable\n\n            public void Dispose()\n            {\n                if (_autoReloadTimer is not null)\n                {\n                    _autoReloadTimer.Dispose();\n                    _autoReloadTimer = null;\n                }\n            }\n\n            #endregion\n\n            #region protected\n\n            protected abstract void ReloadBlockListFile();\n\n            protected static string PopWord(ref string line)\n            {\n                if (line.Length == 0)\n                    return line;\n\n                line = line.TrimStart(_popWordSeperator);\n\n                int i = line.IndexOfAny(_popWordSeperator);\n                string word;\n\n                if (i < 0)\n                {\n                    word = line;\n                    line = \"\";\n                }\n                else\n                {\n                    word = line.Substring(0, i);\n                    line = line.Substring(i + 1);\n                }\n\n                return word;\n            }\n\n            #endregion\n\n            #region public\n\n            public void ReloadConfig(JsonElement jsonBlockList)\n            {\n                _enabled = jsonBlockList.GetPropertyValue(\"enabled\", true);\n                _responseA = IPAddress.Parse(jsonBlockList.GetPropertyValue(\"responseA\", \"127.0.0.2\"));\n\n                if (jsonBlockList.TryGetProperty(\"responseTXT\", out JsonElement jsonResponseTXT))\n                    _responseTXT = jsonResponseTXT.GetString();\n                else\n                    _responseTXT = null;\n\n                string blockListFile = jsonBlockList.GetProperty(\"blockListFile\").GetString();\n\n                if (!Path.IsPathRooted(blockListFile))\n                    blockListFile = Path.Combine(_dnsServer.ApplicationFolder, blockListFile);\n\n                if (!blockListFile.Equals(_blockListFile))\n                {\n                    _blockListFile = blockListFile;\n                    _blockListFileLastModified = default;\n                }\n\n                _autoReloadTimer.Change(0, Timeout.Infinite);\n            }\n\n            public virtual bool IsBlocked(IPAddress address, out IPAddress responseA, out string responseTXT)\n            {\n                throw new InvalidOperationException();\n            }\n\n            public virtual bool IsBlocked(string domain, out string foundDomain, out IPAddress responseA, out string responseTXT)\n            {\n                throw new InvalidOperationException();\n            }\n\n            #endregion\n\n            #region properties\n\n            public BlockListType Type\n            { get { return _type; } }\n\n            public string Name\n            { get { return _name; } }\n\n            public bool Enabled\n            { get { return _enabled; } }\n\n            public IPAddress ResponseA\n            { get { return _responseA; } }\n\n            public string ResponseTXT\n            { get { return _responseTXT; } }\n\n            public string BlockListFile\n            { get { return _blockListFile; } }\n\n            #endregion\n        }\n\n        class BlockEntry<T>\n        {\n            #region variables\n\n            readonly T _key;\n            readonly IPAddress _responseA;\n            readonly string _responseTXT;\n\n            #endregion\n\n            #region constructor\n\n            public BlockEntry(T key, string responseA, string responseTXT)\n            {\n                _key = key;\n\n                if (IPAddress.TryParse(responseA, out IPAddress addr))\n                    _responseA = addr;\n\n                if (!string.IsNullOrEmpty(responseTXT))\n                    _responseTXT = responseTXT;\n            }\n\n            #endregion\n\n            #region properties\n\n            public T Key\n            { get { return _key; } }\n\n            public IPAddress ResponseA\n            { get { return _responseA; } }\n\n            public string ResponseTXT\n            { get { return _responseTXT; } }\n\n            #endregion\n        }\n\n        class IpBlockList : BlockList\n        {\n            #region variables\n\n            Dictionary<IPAddress, BlockEntry<IPAddress>> _ipv4Map;\n            Dictionary<IPAddress, BlockEntry<IPAddress>> _ipv6Map;\n            NetworkMap<BlockEntry<NetworkAddress>> _ipv4NetworkMap;\n            NetworkMap<BlockEntry<NetworkAddress>> _ipv6NetworkMap;\n\n            #endregion\n\n            #region constructor\n\n            public IpBlockList(IDnsServer dnsServer, JsonElement jsonBlockList)\n                : base(dnsServer, BlockListType.Ip, jsonBlockList)\n            { }\n\n            #endregion\n\n            #region protected\n\n            protected override void ReloadBlockListFile()\n            {\n                try\n                {\n                    _dnsServer.WriteLog(\"The app is reading IP block list file: \" + _blockListFile);\n\n                    //parse ip block list file\n                    Queue<BlockEntry<IPAddress>> ipv4Addresses = new Queue<BlockEntry<IPAddress>>();\n                    Queue<BlockEntry<IPAddress>> ipv6Addresses = new Queue<BlockEntry<IPAddress>>();\n                    Queue<BlockEntry<NetworkAddress>> ipv4Networks = new Queue<BlockEntry<NetworkAddress>>();\n                    Queue<BlockEntry<NetworkAddress>> ipv6Networks = new Queue<BlockEntry<NetworkAddress>>();\n\n                    ipv4Addresses.Enqueue(new BlockEntry<IPAddress>(IPAddress.Parse(\"127.0.0.2\"), \"127.0.0.2\", \"rfc5782 test entry\"));\n                    ipv6Addresses.Enqueue(new BlockEntry<IPAddress>(IPAddress.Parse(\"::FFFF:7F00:2\"), \"127.0.0.2\", \"rfc5782 test entry\"));\n\n                    using (FileStream fS = new FileStream(_blockListFile, FileMode.Open, FileAccess.Read))\n                    {\n                        StreamReader sR = new StreamReader(fS, true);\n                        string line;\n                        string network;\n                        string responseA;\n                        string responseTXT;\n\n                        while (true)\n                        {\n                            line = sR.ReadLine();\n                            if (line is null)\n                                break; //eof\n\n                            line = line.TrimStart(_popWordSeperator);\n\n                            if (line.Length == 0)\n                                continue; //skip empty line\n\n                            if (line.StartsWith('#'))\n                                continue; //skip comment line\n\n                            network = PopWord(ref line);\n                            responseA = PopWord(ref line);\n                            responseTXT = line;\n\n                            if (NetworkAddress.TryParse(network, out NetworkAddress networkAddress))\n                            {\n                                switch (networkAddress.AddressFamily)\n                                {\n                                    case AddressFamily.InterNetwork:\n                                        if (networkAddress.PrefixLength == 32)\n                                            ipv4Addresses.Enqueue(new BlockEntry<IPAddress>(networkAddress.Address, responseA, responseTXT));\n                                        else\n                                            ipv4Networks.Enqueue(new BlockEntry<NetworkAddress>(networkAddress, responseA, responseTXT));\n\n                                        break;\n\n                                    case AddressFamily.InterNetworkV6:\n                                        if (networkAddress.PrefixLength == 128)\n                                            ipv6Addresses.Enqueue(new BlockEntry<IPAddress>(networkAddress.Address, responseA, responseTXT));\n                                        else\n                                            ipv6Networks.Enqueue(new BlockEntry<NetworkAddress>(networkAddress, responseA, responseTXT));\n\n                                        break;\n                                }\n                            }\n                        }\n\n                        _blockListFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle);\n                    }\n\n                    //load ip lookup list\n                    Dictionary<IPAddress, BlockEntry<IPAddress>> ipv4AddressMap = new Dictionary<IPAddress, BlockEntry<IPAddress>>(ipv4Addresses.Count);\n\n                    while (ipv4Addresses.Count > 0)\n                    {\n                        BlockEntry<IPAddress> entry = ipv4Addresses.Dequeue();\n                        ipv4AddressMap.TryAdd(entry.Key, entry);\n                    }\n\n                    Dictionary<IPAddress, BlockEntry<IPAddress>> ipv6AddressMap = new Dictionary<IPAddress, BlockEntry<IPAddress>>(ipv6Addresses.Count);\n\n                    while (ipv6Addresses.Count > 0)\n                    {\n                        BlockEntry<IPAddress> entry = ipv6Addresses.Dequeue();\n                        ipv6AddressMap.TryAdd(entry.Key, entry);\n                    }\n\n                    NetworkMap<BlockEntry<NetworkAddress>> ipv4NetworkMap = new NetworkMap<BlockEntry<NetworkAddress>>(ipv4Networks.Count);\n\n                    while (ipv4Networks.Count > 0)\n                    {\n                        BlockEntry<NetworkAddress> entry = ipv4Networks.Dequeue();\n                        ipv4NetworkMap.Add(entry.Key, entry);\n                    }\n\n                    NetworkMap<BlockEntry<NetworkAddress>> ipv6NetworkMap = new NetworkMap<BlockEntry<NetworkAddress>>(ipv6Networks.Count);\n\n                    while (ipv6Networks.Count > 0)\n                    {\n                        BlockEntry<NetworkAddress> entry = ipv6Networks.Dequeue();\n                        ipv6NetworkMap.Add(entry.Key, entry);\n                    }\n\n                    //update\n                    _ipv4Map = ipv4AddressMap;\n                    _ipv6Map = ipv6AddressMap;\n                    _ipv4NetworkMap = ipv4NetworkMap;\n                    _ipv6NetworkMap = ipv6NetworkMap;\n\n                    _dnsServer.WriteLog(\"The app has successfully loaded IP block list file: \" + _blockListFile);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(\"The app failed to read IP block list file: \" + _blockListFile + \"\\r\\n\" + ex.ToString());\n                }\n            }\n\n            #endregion\n\n            #region public\n\n            public override bool IsBlocked(IPAddress address, out IPAddress responseA, out string responseTXT)\n            {\n                switch (address.AddressFamily)\n                {\n                    case AddressFamily.InterNetwork:\n                        {\n                            if (_ipv4Map.TryGetValue(address, out BlockEntry<IPAddress> ipEntry))\n                            {\n                                responseA = ipEntry.ResponseA is null ? _responseA : ipEntry.ResponseA;\n                                responseTXT = ipEntry.ResponseTXT is null ? _responseTXT : ipEntry.ResponseTXT;\n                                return true;\n                            }\n\n                            if (_ipv4NetworkMap.TryGetValue(address, out BlockEntry<NetworkAddress> networkEntry))\n                            {\n                                responseA = networkEntry.ResponseA is null ? _responseA : networkEntry.ResponseA;\n                                responseTXT = networkEntry.ResponseTXT is null ? _responseTXT : networkEntry.ResponseTXT;\n                                return true;\n                            }\n                        }\n                        break;\n\n                    case AddressFamily.InterNetworkV6:\n                        {\n                            if (_ipv6Map.TryGetValue(address, out BlockEntry<IPAddress> ipEntry))\n                            {\n                                responseA = ipEntry.ResponseA is null ? _responseA : ipEntry.ResponseA;\n                                responseTXT = ipEntry.ResponseTXT is null ? _responseTXT : ipEntry.ResponseTXT;\n                                return true;\n                            }\n\n                            if (_ipv6NetworkMap.TryGetValue(address, out BlockEntry<NetworkAddress> networkEntry))\n                            {\n                                responseA = networkEntry.ResponseA is null ? _responseA : networkEntry.ResponseA;\n                                responseTXT = networkEntry.ResponseTXT is null ? _responseTXT : networkEntry.ResponseTXT;\n                                return true;\n                            }\n                        }\n                        break;\n                }\n\n                responseA = null;\n                responseTXT = null;\n                return false;\n            }\n\n            #endregion\n        }\n\n        class DomainBlockList : BlockList\n        {\n            #region variables\n\n            Dictionary<string, BlockEntry<string>> _domainMap;\n\n            #endregion\n\n            #region constructor\n\n            public DomainBlockList(IDnsServer dnsServer, JsonElement jsonIpBlockList)\n                : base(dnsServer, BlockListType.Domain, jsonIpBlockList)\n            { }\n\n            #endregion\n\n            #region protected\n\n            protected override void ReloadBlockListFile()\n            {\n                try\n                {\n                    _dnsServer.WriteLog(\"The app is reading domain block list file: \" + _blockListFile);\n\n                    //parse ip block list file\n                    Queue<BlockEntry<string>> domains = new Queue<BlockEntry<string>>();\n\n                    domains.Enqueue(new BlockEntry<string>(\"test\", \"127.0.0.2\", \"rfc5782 test entry\"));\n\n                    using (FileStream fS = new FileStream(_blockListFile, FileMode.Open, FileAccess.Read))\n                    {\n                        StreamReader sR = new StreamReader(fS, true);\n                        char[] trimSeperator = new char[] { ' ', '\\t', ':', '|', ',' };\n                        string line;\n                        string domain;\n                        string responseA;\n                        string responseTXT;\n\n                        while (true)\n                        {\n                            line = sR.ReadLine();\n                            if (line is null)\n                                break; //eof\n\n                            line = line.TrimStart(trimSeperator);\n\n                            if (line.Length == 0)\n                                continue; //skip empty line\n\n                            if (line.StartsWith('#'))\n                                continue; //skip comment line\n\n                            domain = PopWord(ref line);\n                            responseA = PopWord(ref line);\n                            responseTXT = line;\n\n                            if (DnsClient.IsDomainNameValid(domain))\n                                domains.Enqueue(new BlockEntry<string>(domain.ToLowerInvariant(), responseA, responseTXT));\n                        }\n\n                        _blockListFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle);\n                    }\n\n                    //load ip lookup list\n                    Dictionary<string, BlockEntry<string>> domainMap = new Dictionary<string, BlockEntry<string>>(domains.Count);\n\n                    while (domains.Count > 0)\n                    {\n                        BlockEntry<string> entry = domains.Dequeue();\n                        domainMap.TryAdd(entry.Key, entry);\n                    }\n\n                    //update\n                    _domainMap = domainMap;\n\n                    _dnsServer.WriteLog(\"The app has successfully loaded domain block list file: \" + _blockListFile);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(\"The app failed to read domain block list file: \" + _blockListFile + \"\\r\\n\" + ex.ToString());\n                }\n            }\n\n            #endregion\n\n            #region private\n\n            private static string GetParentZone(string domain)\n            {\n                int i = domain.IndexOf('.');\n                if (i > -1)\n                    return domain.Substring(i + 1);\n\n                //dont return root zone\n                return null;\n            }\n\n            private bool IsDomainBlocked(string domain, out BlockEntry<string> domainEntry)\n            {\n                do\n                {\n                    if (_domainMap.TryGetValue(domain, out domainEntry))\n                    {\n                        return true;\n                    }\n\n                    domain = GetParentZone(domain);\n                }\n                while (domain is not null);\n\n                return false;\n            }\n\n            #endregion\n\n            #region public\n\n            public override bool IsBlocked(string domain, out string foundDomain, out IPAddress responseA, out string responseTXT)\n            {\n                if (IsDomainBlocked(domain.ToLowerInvariant(), out BlockEntry<string> domainEntry))\n                {\n                    foundDomain = domainEntry.Key;\n                    responseA = domainEntry.ResponseA is null ? _responseA : domainEntry.ResponseA;\n                    responseTXT = domainEntry.ResponseTXT is null ? _responseTXT : domainEntry.ResponseTXT;\n                    return true;\n                }\n\n                foundDomain = null;\n                responseA = null;\n                responseTXT = null;\n                return false;\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/DnsBlockListApp/DnsBlockListApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>DnsBlockListApp</AssemblyName>\n\t\t<RootNamespace>DnsBlockList</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"domain-blocklist.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"ip-blocklist.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/DnsBlockListApp/dnsApp.config",
    "content": "{\n  \"dnsBlockLists\": [\n    {\n      \"name\": \"ipblocklist1\",\n      \"type\": \"ip\",\n      \"enabled\": true,\n      \"responseA\": \"127.0.0.2\",\n      \"responseTXT\": \"https://example.com/dnsbl?ip={ip}\",\n      \"blockListFile\": \"ip-blocklist.txt\"\n    },\n    {\n      \"name\": \"domainblocklist1\",\n      \"type\": \"domain\",\n      \"enabled\": true,\n      \"responseA\": \"127.0.0.2\",\n      \"responseTXT\": \"https://example.com/dnsbl?domain={domain}\",\n      \"blockListFile\": \"domain-blocklist.txt\"\n    }\n  ]\n}"
  },
  {
    "path": "Apps/DnsBlockListApp/domain-blocklist.txt",
    "content": "# DNSBL domain block list\n# Format: domain A-response TXT-response\n# Seperator: <space>, <tab>, or <pipe> char\n# \n# A-response & TXT-response are optional but A-response must exists when TXT-response is specified\n# \n# Examples:\n# example.com\n# example.net\t127.0.0.4\n# malware.com\t127.0.0.4\tmalware see: https://example.com/dnsbl?domain={domain}\n"
  },
  {
    "path": "Apps/DnsBlockListApp/ip-blocklist.txt",
    "content": "# DNSBL IP block list\n# Format: ip/network A-response TXT-response\n# Seperator: <space>, <tab>, or <pipe> char\n#\n# A-response & TXT-response are optional but A-response must exists when TXT-response is specified.\n# Supports both IPv4 and IPv6 addresses.\n# \n# Examples:\n# 192.168.1.1\n# 192.168.0.0/24\n# 192.168.2.1\t127.0.0.3\n# 10.8.1.0/24\t127.0.0.3\tmalware see: https://example.com/dnsbl?ip={ip}\n# 2001:db8::/64\n"
  },
  {
    "path": "Apps/DnsRebindingProtectionApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsRebindingProtection\n{\n    public sealed class App : IDnsApplication, IDnsPostProcessor\n    {\n        #region variables\n\n        bool _enableProtection;\n        NetworkAddress[] _bypassNetworks;\n        HashSet<NetworkAddress> _privateNetworks;\n        HashSet<string> _privateDomains;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            // Nothing to dispose of.\n        }\n\n        #endregion\n\n        #region private\n\n        private static string GetParentZone(string domain)\n        {\n            int i = domain.IndexOf('.');\n            if (i > -1)\n                return domain[(i + 1)..];\n\n            //dont return root zone\n            return null;\n        }\n\n        private bool IsPrivateDomain(string domain)\n        {\n            domain = domain.ToLowerInvariant();\n\n            do\n            {\n                if (_privateDomains.Contains(domain))\n                    return true;\n\n                domain = GetParentZone(domain);\n            }\n            while (domain is not null);\n\n            return false;\n        }\n\n        private bool IsRebindingAttempt(DnsResourceRecord record)\n        {\n            IPAddress address;\n\n            switch (record.Type)\n            {\n                case DnsResourceRecordType.A:\n                    if (IsPrivateDomain(record.Name))\n                        return false;\n\n                    address = (record.RDATA as DnsARecordData).Address;\n                    break;\n\n                case DnsResourceRecordType.AAAA:\n                    if (IsPrivateDomain(record.Name))\n                        return false;\n\n                    address = (record.RDATA as DnsAAAARecordData).Address;\n                    break;\n\n                default:\n                    return false;\n            }\n\n            foreach (NetworkAddress networkAddress in _privateNetworks)\n            {\n                if (networkAddress.Contains(address))\n                    return true;\n            }\n\n            return false;\n        }\n\n        private bool TryDetectRebinding(IReadOnlyList<DnsResourceRecord> answer, out List<DnsResourceRecord> protectedAnswer)\n        {\n            for (int i = 0; i < answer.Count; i++)\n            {\n                DnsResourceRecord record = answer[i];\n                if (IsRebindingAttempt(record))\n                {\n                    //rebinding attempt detected!\n                    //prepare protected answer\n                    protectedAnswer = new List<DnsResourceRecord>(answer.Count);\n\n                    //copy passed records\n                    for (int j = 0; j < i; j++)\n                        protectedAnswer.Add(answer[j]);\n\n                    //copy remaining records with check\n                    for (int j = i + 1; j < answer.Count; j++)\n                    {\n                        record = answer[j];\n                        if (!IsRebindingAttempt(record))\n                            protectedAnswer.Add(record);\n                    }\n\n                    return true;\n                }\n            }\n\n            protectedAnswer = null;\n            return false;\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _enableProtection = jsonConfig.GetPropertyValue(\"enableProtection\", true);\n            _privateNetworks = new HashSet<NetworkAddress>(jsonConfig.ReadArray(\"privateNetworks\", NetworkAddress.Parse));\n            _privateDomains = new HashSet<string>(jsonConfig.ReadArray(\"privateDomains\"));\n\n            if (jsonConfig.TryReadArray(\"bypassNetworks\", NetworkAddress.Parse, out NetworkAddress[] bypassNetworks))\n            {\n                _bypassNetworks = bypassNetworks;\n            }\n            else\n            {\n                _bypassNetworks = [];\n\n                //update config for new feature\n                config = config.Replace(\"\\\"privateNetworks\\\"\", \"\\\"bypassNetworks\\\": [\\r\\n  ],\\r\\n  \\\"privateNetworks\\\"\");\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n        }\n\n        public Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            // Do not filter authoritative responses. Because in this case any rebinding is intentional.\n            if (!_enableProtection || response.AuthoritativeAnswer)\n                return Task.FromResult(response);\n\n            IPAddress remoteIP = remoteEP.Address;\n\n            foreach (NetworkAddress network in _bypassNetworks)\n            {\n                if (network.Contains(remoteIP))\n                    return Task.FromResult(response);\n            }\n\n            if (TryDetectRebinding(response.Answer, out List<DnsResourceRecord> protectedAnswer))\n                return Task.FromResult(response.Clone(protectedAnswer));\n\n            return Task.FromResult(response);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Protects from DNS rebinding attacks using configured private domains and networks.\"; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/DnsRebindingProtectionApp/DnsRebindingProtectionApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare, Rui Fung Yip</Authors>\n    <AssemblyName>DnsRebindingProtectionApp</AssemblyName>\n    <RootNamespace>DnsRebindingProtection</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <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.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/DnsRebindingProtectionApp/dnsApp.config",
    "content": "﻿{\n  \"enableProtection\": true,\n  \"bypassNetworks\": [\n  ],\n  \"privateNetworks\": [\n    \"10.0.0.0/8\",\n    \"127.0.0.0/8\",\n    \"172.16.0.0/12\",\n    \"192.168.0.0/16\",\n    \"169.254.0.0/16\",\n    \"fc00::/7\",\n    \"fe80::/10\"\n  ],\n  \"privateDomains\": [\n    \"home.arpa\"\n  ]\n}"
  },
  {
    "path": "Apps/DropRequestsApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.IO;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DropRequests\n{\n    public sealed class App : IDnsApplication, IDnsRequestController\n    {\n        #region variables\n\n        bool _enableBlocking;\n        bool _dropMalformedRequests;\n        NetworkAddress[] _allowedNetworks;\n        NetworkAddress[] _blockedNetworks;\n        BlockedQuestion[] _blockedQuestions;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _enableBlocking = jsonConfig.GetProperty(\"enableBlocking\").GetBoolean();\n\n            if (jsonConfig.TryGetProperty(\"dropMalformedRequests\", out JsonElement jsonDropMalformedRequests))\n                _dropMalformedRequests = jsonDropMalformedRequests.GetBoolean();\n            else\n                _dropMalformedRequests = false;\n\n            if (jsonConfig.TryReadArray(\"allowedNetworks\", NetworkAddress.Parse, out NetworkAddress[] allowedNetworks))\n                _allowedNetworks = allowedNetworks;\n            else\n                _allowedNetworks = Array.Empty<NetworkAddress>();\n\n            if (jsonConfig.TryReadArray(\"blockedNetworks\", NetworkAddress.Parse, out NetworkAddress[] blockedNetworks))\n                _blockedNetworks = blockedNetworks;\n            else\n                _blockedNetworks = Array.Empty<NetworkAddress>();\n\n            if (jsonConfig.TryReadArray(\"blockedQuestions\", delegate (JsonElement blockedQuestion) { return new BlockedQuestion(blockedQuestion); }, out BlockedQuestion[] blockedQuestions))\n                _blockedQuestions = blockedQuestions;\n            else\n                _blockedQuestions = Array.Empty<BlockedQuestion>();\n\n            if (!jsonConfig.TryGetProperty(\"dropMalformedRequests\", out _))\n            {\n                config = config.Replace(\"\\\"allowedNetworks\\\"\", \"\\\"dropMalformedRequests\\\": false,\\r\\n  \\\"allowedNetworks\\\"\");\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n        }\n\n        public Task<DnsRequestControllerAction> GetRequestActionAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol)\n        {\n            if (!_enableBlocking)\n                return Task.FromResult(DnsRequestControllerAction.Allow);\n\n            if (_dropMalformedRequests && (request.ParsingException is not null))\n                return Task.FromResult(DnsRequestControllerAction.DropSilently);\n\n            IPAddress remoteIp = remoteEP.Address;\n\n            foreach (NetworkAddress allowedNetwork in _allowedNetworks)\n            {\n                if (allowedNetwork.Contains(remoteIp))\n                    return Task.FromResult(DnsRequestControllerAction.Allow);\n            }\n\n            foreach (NetworkAddress blockedNetwork in _blockedNetworks)\n            {\n                if (blockedNetwork.Contains(remoteIp))\n                    return Task.FromResult(DnsRequestControllerAction.DropSilently);\n            }\n\n            if (request.Question.Count != 1)\n                return Task.FromResult(DnsRequestControllerAction.DropSilently);\n\n            DnsQuestionRecord requestQuestion = request.Question[0];\n\n            foreach (BlockedQuestion blockedQuestion in _blockedQuestions)\n            {\n                if (blockedQuestion.Matches(requestQuestion))\n                    return Task.FromResult(DnsRequestControllerAction.DropSilently);\n            }\n\n            return Task.FromResult(DnsRequestControllerAction.Allow);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Drops incoming DNS requests that match list of blocked networks or blocked questions.\"; } }\n\n        #endregion\n\n        class BlockedQuestion\n        {\n            #region variables\n\n            readonly string _name;\n            readonly bool _blockZone;\n            readonly DnsResourceRecordType _type;\n\n            #endregion\n\n            #region constructor\n\n            public BlockedQuestion(JsonElement jsonQuestion)\n            {\n                if (jsonQuestion.TryGetProperty(\"name\", out JsonElement jsonName))\n                    _name = jsonName.GetString().TrimEnd('.');\n\n                if (jsonQuestion.TryGetProperty(\"blockZone\", out JsonElement jsonBlockZone))\n                    _blockZone = jsonBlockZone.GetBoolean();\n\n                if (jsonQuestion.TryGetProperty(\"type\", out JsonElement jsonType))\n                {\n                    if (!Enum.TryParse(jsonType.GetString(), true, out DnsResourceRecordType type))\n                        throw new NotSupportedException(\"DNS record type is not supported: \" + jsonType.GetString());\n\n                    _type = type;\n                }\n                else\n                {\n                    _type = DnsResourceRecordType.Unknown;\n                }\n            }\n\n            #endregion\n\n            #region public\n\n            public bool Matches(DnsQuestionRecord question)\n            {\n                if (_name is not null)\n                {\n                    if (_blockZone)\n                    {\n                        if ((_name.Length > 0) && !_name.Equals(question.Name, StringComparison.OrdinalIgnoreCase) && !question.Name.EndsWith(\".\" + _name, StringComparison.OrdinalIgnoreCase))\n                            return false;\n                    }\n                    else\n                    {\n                        if (!_name.Equals(question.Name, StringComparison.OrdinalIgnoreCase))\n                            return false;\n                    }\n                }\n\n                if ((_type != DnsResourceRecordType.Unknown) && (_type != question.Type))\n                    return false;\n\n                return true;\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/DropRequestsApp/DropRequestsApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>7.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>DropRequestsApp</AssemblyName>\n\t\t<RootNamespace>DropRequests</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<Description>Drops incoming DNS requests that match list of blocked networks or blocked questions.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/DropRequestsApp/dnsApp.config",
    "content": "{\n  \"enableBlocking\": true,\n  \"dropMalformedRequests\": false,\n  \"allowedNetworks\": [\n    \"127.0.0.1\",\n    \"::1\",\n    \"10.0.0.0/8\",\n    \"172.16.0.0/12\",\n    \"192.168.0.0/16\"\n  ],\n  \"blockedNetworks\": [\n  ],\n  \"blockedQuestions\": [\n    {\n      \"name\": \"example.com\",\n      \"blockZone\": true\n    },\n    {\n      \"type\": \"ANY\"\n    },\n    {\n      \"name\": \"pizzaseo.com\",\n      \"type\": \"RRSIG\"\n    },\n    {\n      \"name\": \"sl\",\n      \"type\": \"ANY\"\n    },\n    {\n      \"name\": \"a.a.a.ooooops.space\",\n      \"type\": \"A\"\n    }\n  ]\n}"
  },
  {
    "path": "Apps/FailoverApp/Address.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace Failover\n{\n    enum FailoverType\n    {\n        Unknown = 0,\n        Primary = 1,\n        Secondary = 2\n    }\n\n    public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        HealthService _healthService;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            if (_healthService is not null)\n                _healthService.Dispose();\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region private\n\n        private void GetAnswers(JsonElement jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List<DnsResourceRecord> answers)\n        {\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                    foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())\n                    {\n                        IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                        if (address.AddressFamily == AddressFamily.InterNetwork)\n                        {\n                            HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true);\n                            switch (response.Status)\n                            {\n                                case HealthStatus.Unknown:\n                                    answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 10, new DnsARecordData(address)));\n                                    break;\n\n                                case HealthStatus.Healthy:\n                                    answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecordData(address)));\n                                    break;\n                            }\n                        }\n                    }\n                    break;\n\n                case DnsResourceRecordType.AAAA:\n                    foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())\n                    {\n                        IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                        if (address.AddressFamily == AddressFamily.InterNetworkV6)\n                        {\n                            HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true);\n                            switch (response.Status)\n                            {\n                                case HealthStatus.Unknown:\n                                    answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 10, new DnsAAAARecordData(address)));\n                                    break;\n\n                                case HealthStatus.Healthy:\n                                    answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, appRecordTtl, new DnsAAAARecordData(address)));\n                                    break;\n                            }\n                        }\n                    }\n                    break;\n            }\n        }\n\n        private void GetStatusAnswers(JsonElement jsonAddresses, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List<DnsResourceRecord> answers)\n        {\n            foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())\n            {\n                IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n                HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, false);\n\n                string text = \"app=failover; addressType=\" + type.ToString() + \"; address=\" + address.ToString() + \"; healthCheck=\" + healthCheck + (healthCheckUrl is null ? \"\" : \"; healthCheckUrl=\" + healthCheckUrl.AbsoluteUri) + \"; healthStatus=\" + response.Status.ToString() + \";\";\n\n                if (response.Status == HealthStatus.Failed)\n                    text += \" failureReason=\" + response.FailureReason + \";\";\n\n                answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(text)));\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            if (_healthService is null)\n                _healthService = HealthService.Create(dnsServer);\n\n            _healthService.Initialize(config);\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    {\n                        using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n                        JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n                        string healthCheck = jsonAppRecordData.GetPropertyValue(\"healthCheck\", null);\n                        Uri healthCheckUrl = null;\n\n                        if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null))\n                        {\n                            //read health check url only for http/https type checks and only when app config does not have an url configured\n                            if (jsonAppRecordData.TryGetProperty(\"healthCheckUrl\", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null))\n                            {\n                                healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString());\n                            }\n                            else\n                            {\n                                if (hc.Type == HealthCheckType.Https)\n                                    healthCheckUrl = new Uri(\"https://\" + question.Name);\n                                else\n                                    healthCheckUrl = new Uri(\"http://\" + question.Name);\n                            }\n                        }\n\n                        List<DnsResourceRecord> answers = new List<DnsResourceRecord>();\n\n                        if (jsonAppRecordData.TryGetProperty(\"primary\", out JsonElement jsonPrimary))\n                            GetAnswers(jsonPrimary, question, appRecordTtl, healthCheck, healthCheckUrl, answers);\n\n                        if (answers.Count == 0)\n                        {\n                            if (jsonAppRecordData.TryGetProperty(\"secondary\", out JsonElement jsonSecondary))\n                                GetAnswers(jsonSecondary, question, appRecordTtl, healthCheck, healthCheckUrl, answers);\n\n                            if (answers.Count == 0)\n                            {\n                                if (jsonAppRecordData.TryGetProperty(\"serverDown\", out JsonElement jsonServerDown))\n                                {\n                                    if (question.Type == DnsResourceRecordType.A)\n                                    {\n                                        foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray())\n                                        {\n                                            IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                            if (address.AddressFamily == AddressFamily.InterNetwork)\n                                                answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecordData(address)));\n                                        }\n                                    }\n                                    else\n                                    {\n                                        foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray())\n                                        {\n                                            IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                            if (address.AddressFamily == AddressFamily.InterNetworkV6)\n                                                answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecordData(address)));\n                                        }\n                                    }\n                                }\n\n                                if (answers.Count == 0)\n                                    return Task.FromResult<DnsDatagram>(null);\n                            }\n                        }\n\n                        if (answers.Count > 1)\n                            answers.Shuffle();\n\n                        return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));\n                    }\n\n                case DnsResourceRecordType.TXT:\n                    {\n                        using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n                        JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n                        bool allowTxtStatus = jsonAppRecordData.GetPropertyValue(\"allowTxtStatus\", false);\n                        if (!allowTxtStatus)\n                            return Task.FromResult<DnsDatagram>(null);\n\n                        string healthCheck = jsonAppRecordData.GetPropertyValue(\"healthCheck\", null);\n                        Uri healthCheckUrl = null;\n\n                        if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null))\n                        {\n                            //read health check url only for http/https type checks and only when app config does not have an url configured\n                            if (jsonAppRecordData.TryGetProperty(\"healthCheckUrl\", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null))\n                            {\n                                healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString());\n                            }\n                            else\n                            {\n                                if (hc.Type == HealthCheckType.Https)\n                                    healthCheckUrl = new Uri(\"https://\" + question.Name);\n                                else\n                                    healthCheckUrl = new Uri(\"http://\" + question.Name);\n                            }\n                        }\n\n                        List<DnsResourceRecord> answers = new List<DnsResourceRecord>();\n\n                        if (jsonAppRecordData.TryGetProperty(\"primary\", out JsonElement jsonPrimary))\n                            GetStatusAnswers(jsonPrimary, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, answers);\n\n                        if (jsonAppRecordData.TryGetProperty(\"secondary\", out JsonElement jsonSecondary))\n                            GetStatusAnswers(jsonSecondary, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, answers);\n\n                        return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));\n                    }\n\n                default:\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"primary\"\": [\n    \"\"1.1.1.1\"\",\n    \"\"::1\"\"\n  ],\n  \"\"secondary\"\": [\n    \"\"2.2.2.2\"\",\n    \"\"::2\"\"\n  ],\n  \"\"serverDown\"\": [\n    \"\"3.3.3.3\"\"\n  ],\n  \"\"healthCheck\"\": \"\"https\"\",\n  \"\"healthCheckUrl\"\": \"\"https://www.example.com/\"\",\n  \"\"allowTxtStatus\"\": false\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/CNAME.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace Failover\n{\n    public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        HealthService _healthService;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            if (_healthService is not null)\n                _healthService.Dispose();\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region private\n\n        private DnsResourceRecord[] GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck, Uri healthCheckUrl)\n        {\n            DnsResourceRecordType healthCheckRecordType;\n\n            if (question.Type == DnsResourceRecordType.AAAA)\n                healthCheckRecordType = DnsResourceRecordType.AAAA;\n            else\n                healthCheckRecordType = DnsResourceRecordType.A;\n\n            HealthCheckResponse response = _healthService.QueryStatus(domain, healthCheckRecordType, healthCheck, healthCheckUrl, true);\n            switch (response.Status)\n            {\n                case HealthStatus.Unknown:\n                    if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                        return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 10, new DnsANAMERecordData(domain)) }; //use ANAME\n                    else\n                        return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 10, new DnsCNAMERecordData(domain)) };\n\n                case HealthStatus.Healthy:\n                    if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                        return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(domain)) }; //use ANAME\n                    else\n                        return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(domain)) };\n            }\n\n            return null;\n        }\n\n        private void GetStatusAnswers(string domain, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List<DnsResourceRecord> answers)\n        {\n            {\n                HealthCheckResponse response = _healthService.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, healthCheckUrl, false);\n\n                string text = \"app=failover; cnameType=\" + type.ToString() + \"; domain=\" + domain + \"; qType: A; healthCheck=\" + healthCheck + (healthCheckUrl is null ? \"\" : \"; healthCheckUrl=\" + healthCheckUrl.AbsoluteUri) + \"; healthStatus=\" + response.Status.ToString() + \";\";\n\n                if (response.Status == HealthStatus.Failed)\n                    text += \" failureReason=\" + response.FailureReason + \";\";\n\n                answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(text)));\n            }\n\n            {\n                HealthCheckResponse response = _healthService.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, healthCheckUrl, false);\n\n                string text = \"app=failover; cnameType=\" + type.ToString() + \"; domain=\" + domain + \"; qType: AAAA; healthCheck=\" + healthCheck + (healthCheckUrl is null ? \"\" : \"; healthCheckUrl=\" + healthCheckUrl.AbsoluteUri) + \"; healthStatus=\" + response.Status.ToString() + \";\";\n\n                if (response.Status == HealthStatus.Failed)\n                    text += \" failureReason=\" + response.FailureReason + \";\";\n\n                answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(text)));\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            if (_healthService is null)\n                _healthService = HealthService.Create(dnsServer);\n\n            //let Address class initialize config\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n            JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n            string healthCheck = jsonAppRecordData.GetPropertyValue(\"healthCheck\", null);\n            Uri healthCheckUrl = null;\n\n            if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null))\n            {\n                //read health check url only for http/https type checks and only when app config does not have an url configured\n                if (jsonAppRecordData.TryGetProperty(\"healthCheckUrl\", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null))\n                {\n                    healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString());\n                }\n                else\n                {\n                    if (hc.Type == HealthCheckType.Https)\n                        healthCheckUrl = new Uri(\"https://\" + question.Name);\n                    else\n                        healthCheckUrl = new Uri(\"http://\" + question.Name);\n                }\n            }\n\n            IReadOnlyList<DnsResourceRecord> answers = null;\n\n            if (question.Type == DnsResourceRecordType.TXT)\n            {\n                bool allowTxtStatus = jsonAppRecordData.GetPropertyValue(\"allowTxtStatus\", false);\n                if (!allowTxtStatus)\n                    return Task.FromResult<DnsDatagram>(null);\n\n                List<DnsResourceRecord> txtAnswers = new List<DnsResourceRecord>();\n\n                if (jsonAppRecordData.TryGetProperty(\"primary\", out JsonElement jsonPrimary))\n                    GetStatusAnswers(jsonPrimary.GetString(), FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, txtAnswers);\n\n                if (jsonAppRecordData.TryGetProperty(\"secondary\", out JsonElement jsonSecondary))\n                {\n                    foreach (JsonElement jsonDomain in jsonSecondary.EnumerateArray())\n                        GetStatusAnswers(jsonDomain.GetString(), FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, txtAnswers);\n                }\n\n                answers = txtAnswers;\n            }\n            else\n            {\n                if (jsonAppRecordData.TryGetProperty(\"primary\", out JsonElement jsonPrimary))\n                    answers = GetAnswers(jsonPrimary.GetString(), question, zoneName, appRecordTtl, healthCheck, healthCheckUrl);\n\n                if (answers is null)\n                {\n                    if (jsonAppRecordData.TryGetProperty(\"secondary\", out JsonElement jsonSecondary))\n                    {\n                        foreach (JsonElement jsonDomain in jsonSecondary.EnumerateArray())\n                        {\n                            answers = GetAnswers(jsonDomain.GetString(), question, zoneName, appRecordTtl, healthCheck, healthCheckUrl);\n                            if (answers is not null)\n                                break;\n                        }\n                    }\n\n                    if (answers is null)\n                    {\n                        if (!jsonAppRecordData.TryGetProperty(\"serverDown\", out JsonElement jsonServerDown) || (jsonServerDown.ValueKind == JsonValueKind.Null))\n                            return Task.FromResult<DnsDatagram>(null);\n\n                        string serverDown = jsonServerDown.GetString();\n\n                        if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                            answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 30, new DnsANAMERecordData(serverDown)) }; //use ANAME\n                        else\n                            answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 30, new DnsCNAMERecordData(serverDown)) };\n                    }\n                }\n            }\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"primary\"\": \"\"in.example.org\"\",\n  \"\"secondary\"\": [\n    \"\"sg.example.org\"\",\n    \"\"eu.example.org\"\"\n  ],\n  \"\"serverDown\"\": \"\"status.example.org\"\",\n  \"\"healthCheck\"\": \"\"tcp443\"\",\n  \"\"healthCheckUrl\"\": null,\n  \"\"allowTxtStatus\"\": false\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/EmailAlert.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Net;\nusing System.Net.Mail;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Mail;\n\nnamespace Failover\n{\n    class EmailAlert : IDisposable\n    {\n        #region variables\n\n        readonly HealthService _service;\n\n        readonly string _name;\n        bool _enabled;\n        MailAddress[] _alertTo;\n        string _smtpServer;\n        int _smtpPort;\n        bool _startTls;\n        bool _smtpOverTls;\n        string _username;\n        string _password;\n        MailAddress _mailFrom;\n\n        readonly SmtpClientEx _smtpClient;\n\n        #endregion\n\n        #region constructor\n\n        public EmailAlert(HealthService service, JsonElement jsonEmailAlert)\n        {\n            _service = service;\n\n            _smtpClient = new SmtpClientEx();\n            _smtpClient.DnsClient = new DnsClientInternal(_service.DnsServer);\n\n            _name = jsonEmailAlert.GetPropertyValue(\"name\", \"default\");\n\n            Reload(jsonEmailAlert);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_smtpClient is not null)\n                    _smtpClient.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task SendMailAsync(MailMessage message)\n        {\n            try\n            {\n                const int MAX_RETRIES = 3;\n                const int WAIT_INTERVAL = 30000;\n\n                for (int retries = 0; retries < MAX_RETRIES; retries++)\n                {\n                    try\n                    {\n                        await _smtpClient.SendMailAsync(message);\n                        break;\n                    }\n                    catch\n                    {\n                        if (retries == MAX_RETRIES - 1)\n                            throw;\n\n                        await Task.Delay(WAIT_INTERVAL);\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _service.DnsServer.WriteLog(\"Failed to send email alert [\" + _name + \"].\\r\\n\" + ex.ToString());\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void Reload(JsonElement jsonEmailAlert)\n        {\n            _enabled = jsonEmailAlert.GetPropertyValue(\"enabled\", false);\n\n            if (jsonEmailAlert.TryReadArray(\"alertTo\", delegate (string emailAddress) { return new MailAddress(emailAddress); }, out MailAddress[] alertTo))\n                _alertTo = alertTo;\n            else\n                _alertTo = null;\n\n            _smtpServer = jsonEmailAlert.GetPropertyValue(\"smtpServer\", null);\n            _smtpPort = jsonEmailAlert.GetPropertyValue(\"smtpPort\", 25);\n            _startTls = jsonEmailAlert.GetPropertyValue(\"startTls\", false);\n            _smtpOverTls = jsonEmailAlert.GetPropertyValue(\"smtpOverTls\", false);\n            _username = jsonEmailAlert.GetPropertyValue(\"username\", null);\n            _password = jsonEmailAlert.GetPropertyValue(\"password\", null);\n\n            if (jsonEmailAlert.TryGetProperty(\"mailFrom\", out JsonElement jsonMailFrom))\n            {\n                if (jsonEmailAlert.TryGetProperty(\"mailFromName\", out JsonElement jsonMailFromName))\n                    _mailFrom = new MailAddress(jsonMailFrom.GetString(), jsonMailFromName.GetString(), Encoding.UTF8);\n                else\n                    _mailFrom = new MailAddress(jsonMailFrom.GetString());\n            }\n            else\n            {\n                _mailFrom = null;\n            }\n\n            //update smtp client settings\n            _smtpClient.Host = _smtpServer;\n            _smtpClient.Port = _smtpPort;\n            _smtpClient.EnableSsl = _startTls;\n            _smtpClient.SmtpOverTls = _smtpOverTls;\n\n            if (string.IsNullOrEmpty(_username))\n                _smtpClient.Credentials = null;\n            else\n                _smtpClient.Credentials = new NetworkCredential(_username, _password);\n\n            _smtpClient.Proxy = _service.DnsServer.Proxy;\n        }\n\n        public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse)\n        {\n            if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))\n                return Task.CompletedTask;\n\n            MailMessage message = new MailMessage();\n\n            message.From = _mailFrom;\n\n            foreach (MailAddress alertTo in _alertTo)\n                message.To.Add(alertTo);\n\n            message.Subject = \"[Alert] Address [\" + address.ToString() + \"] Status Is \" + healthCheckResponse.Status.ToString().ToUpper();\n\n            switch (healthCheckResponse.Status)\n            {\n                case HealthStatus.Failed:\n                    message.Body = @\"Hi,\n\nThe 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. \n\nAddress: \" + address.ToString() + @\"\nHealth Check: \" + healthCheck + @\"\nStatus: \" + healthCheckResponse.Status.ToString().ToUpper() + @\"\nAlert Time: \" + healthCheckResponse.DateTime.ToString(\"R\") + @\"\nFailure Reason: \" + healthCheckResponse.FailureReason + @\"\n\nRegards,\nDNS Failover App\n\";\n                    break;\n\n                default:\n                    message.Body = @\"Hi,\n\nThe 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() + @\".\n\nAddress: \" + address.ToString() + @\"\nHealth Check: \" + healthCheck + @\"\nStatus: \" + healthCheckResponse.Status.ToString().ToUpper() + @\"\nAlert Time: \" + healthCheckResponse.DateTime.ToString(\"R\") + @\"\n\nRegards,\nDNS Failover App\n\";\n                    break;\n            }\n\n            return SendMailAsync(message);\n        }\n\n        public Task SendAlertAsync(IPAddress address, string healthCheck, Exception ex)\n        {\n            if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))\n                return Task.CompletedTask;\n\n            MailMessage message = new MailMessage();\n\n            message.From = _mailFrom;\n\n            foreach (MailAddress alertTo in _alertTo)\n                message.To.Add(alertTo);\n\n            message.Subject = \"[Alert] Address [\" + address.ToString() + \"] Status Is ERROR\";\n            message.Body = @\"Hi,\n\nThe DNS Failover App has failed to perform a health check [\" + healthCheck + \"] on the address [\" + address.ToString() + @\"]. \n\nAddress: \" + address.ToString() + @\"\nHealth Check: \" + healthCheck + @\"\nStatus: ERROR\nAlert Time: \" + DateTime.UtcNow.ToString(\"R\") + @\"\nFailure Reason: \" + ex.ToString() + @\"\n\nRegards,\nDNS Failover App\n\";\n\n            return SendMailAsync(message);\n        }\n\n        public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse)\n        {\n            if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))\n                return Task.CompletedTask;\n\n            MailMessage message = new MailMessage();\n\n            message.From = _mailFrom;\n\n            foreach (MailAddress alertTo in _alertTo)\n                message.To.Add(alertTo);\n\n            message.Subject = \"[Alert] Domain [\" + domain + \"] Status Is \" + healthCheckResponse.Status.ToString().ToUpper();\n\n            switch (healthCheckResponse.Status)\n            {\n                case HealthStatus.Failed:\n                    message.Body = @\"Hi,\n\nThe 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. \n\nDomain: \" + domain + @\"\nRecord Type: \" + type.ToString() + @\"\nHealth Check: \" + healthCheck + @\"\nStatus: \" + healthCheckResponse.Status.ToString().ToUpper() + @\"\nAlert Time: \" + healthCheckResponse.DateTime.ToString(\"R\") + @\"\nFailure Reason: \" + healthCheckResponse.FailureReason + @\"\n\nRegards,\nDNS Failover App\n\";\n                    break;\n\n                default:\n                    message.Body = @\"Hi,\n\nThe 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() + @\".\n\nDomain: \" + domain + @\"\nRecord Type: \" + type.ToString() + @\"\nHealth Check: \" + healthCheck + @\"\nStatus: \" + healthCheckResponse.Status.ToString().ToUpper() + @\"\nAlert Time: \" + healthCheckResponse.DateTime.ToString(\"R\") + @\"\n\nRegards,\nDNS Failover App\n\";\n                    break;\n            }\n\n            return SendMailAsync(message);\n        }\n\n        public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)\n        {\n            if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))\n                return Task.CompletedTask;\n\n            MailMessage message = new MailMessage();\n\n            message.From = _mailFrom;\n\n            foreach (MailAddress alertTo in _alertTo)\n                message.To.Add(alertTo);\n\n            message.Subject = \"[Alert] Domain [\" + domain + \"] Status Is ERROR\";\n            message.Body = @\"Hi,\n\nThe DNS Failover App has failed to perform a health check [\" + healthCheck + \"] on the domain name [\" + domain + @\"]. \n\nDomain: \" + domain + @\"\nRecord Type: \" + type.ToString() + @\"\nHealth Check: \" + healthCheck + @\"\nStatus: ERROR\nAlert Time: \" + DateTime.UtcNow.ToString(\"R\") + @\"\nFailure Reason: \" + ex.ToString() + @\"\n\nRegards,\nDNS Failover App\n\";\n\n            return SendMailAsync(message);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Name\n        { get { return _name; } }\n\n        public bool Enabled\n        { get { return _enabled; } }\n\n        public MailAddress[] AlertTo\n        { get { return _alertTo; } }\n\n        public string SmtpServer\n        { get { return _smtpServer; } }\n\n        public int SmtpPort\n        { get { return _smtpPort; } }\n\n        public bool StartTls\n        { get { return _startTls; } }\n\n        public bool SmtpOverTls\n        { get { return _smtpOverTls; } }\n\n        public string Username\n        { get { return _username; } }\n\n        public string Password\n        { get { return _password; } }\n\n        public MailAddress MailFrom\n        { get { return _mailFrom; } }\n\n        #endregion\n\n        class DnsClientInternal : IDnsClient\n        {\n            readonly IDnsServer _dnsServer;\n\n            public DnsClientInternal(IDnsServer dnsServer)\n            {\n                _dnsServer = dnsServer;\n            }\n\n            public Task<DnsDatagram> ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default)\n            {\n                return _dnsServer.DirectQueryAsync(question, cancellationToken: cancellationToken);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/FailoverApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>9.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>FailoverApp</AssemblyName>\n\t\t<RootNamespace>Failover</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.IO\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.IO.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net.Mail\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.Mail.dll</HintPath>\n\t\t\t<Private>true</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/FailoverApp/HealthCheck.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Http.Client;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace Failover\n{\n    enum HealthCheckType\n    {\n        Unknown = 0,\n        Ping = 1,\n        Tcp = 2,\n        Http = 3,\n        Https = 4\n    }\n\n    class HealthCheck : IDisposable\n    {\n        #region variables\n\n        const string HTTP_HEALTH_CHECK_USER_AGENT = \"DNS Failover App (Technitium DNS Server)\";\n\n        readonly HealthService _service;\n\n        readonly string _name;\n        HealthCheckType _type;\n        int _interval;\n        int _retries;\n        int _timeout;\n        int _port;\n        Uri _url;\n        EmailAlert _emailAlert;\n        WebHook _webHook;\n\n        HttpClientNetworkHandler _httpHandler;\n        HttpClient _httpClient;\n\n        #endregion\n\n        #region constructor\n\n        public HealthCheck(HealthService service, JsonElement jsonHealthCheck)\n        {\n            _service = service;\n\n            _name = jsonHealthCheck.GetPropertyValue(\"name\", \"default\");\n\n            Reload(jsonHealthCheck);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_httpClient != null)\n                {\n                    _httpClient.Dispose();\n                    _httpClient = null;\n                }\n\n                if (_httpHandler != null)\n                {\n                    _httpHandler.Dispose();\n                    _httpHandler = null;\n                }\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private void ConditionalHttpReload()\n        {\n            switch (_type)\n            {\n                case HealthCheckType.Http:\n                case HealthCheckType.Https:\n                    bool handlerChanged = false;\n                    NetProxy proxy = _service.DnsServer.Proxy;\n\n                    if (_httpHandler is null)\n                    {\n                        HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler();\n                        httpHandler.Proxy = proxy;\n                        httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                        httpHandler.DnsClient = _service.DnsServer;\n\n                        httpHandler.InnerHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);\n                        httpHandler.InnerHandler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(Math.Max(10000, _timeout));\n                        httpHandler.InnerHandler.AllowAutoRedirect = false;\n\n                        _httpHandler = httpHandler;\n                        handlerChanged = true;\n                    }\n                    else\n                    {\n                        if ((_httpHandler.InnerHandler.ConnectTimeout.TotalMilliseconds != _timeout) || (_httpHandler.Proxy != proxy))\n                        {\n                            HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler();\n                            httpHandler.Proxy = proxy;\n                            httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                            httpHandler.DnsClient = _service.DnsServer;\n\n                            httpHandler.InnerHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);\n                            httpHandler.InnerHandler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(Math.Max(10000, _timeout));\n                            httpHandler.InnerHandler.AllowAutoRedirect = false;\n\n                            HttpClientNetworkHandler oldHttpHandler = _httpHandler;\n                            _httpHandler = httpHandler;\n                            handlerChanged = true;\n\n                            oldHttpHandler.Dispose();\n                        }\n                    }\n\n                    if (_httpClient is null)\n                    {\n                        HttpClient httpClient = new HttpClient(_httpHandler);\n                        httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);\n                        httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(HTTP_HEALTH_CHECK_USER_AGENT);\n                        httpClient.DefaultRequestHeaders.ConnectionClose = true;\n\n                        _httpClient = httpClient;\n                    }\n                    else\n                    {\n                        if (handlerChanged || (_httpClient.Timeout.TotalMilliseconds != _timeout))\n                        {\n                            HttpClient httpClient = new HttpClient(_httpHandler);\n                            httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);\n                            httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(HTTP_HEALTH_CHECK_USER_AGENT);\n                            httpClient.DefaultRequestHeaders.ConnectionClose = true;\n\n                            HttpClient oldHttpClient = _httpClient;\n                            _httpClient = httpClient;\n\n                            oldHttpClient.Dispose();\n                        }\n                    }\n                    break;\n\n                default:\n                    if (_httpClient != null)\n                    {\n                        _httpClient.Dispose();\n                        _httpClient = null;\n                    }\n\n                    if (_httpHandler != null)\n                    {\n                        _httpHandler.Dispose();\n                        _httpHandler = null;\n                    }\n                    break;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void Reload(JsonElement jsonHealthCheck)\n        {\n            _type = Enum.Parse<HealthCheckType>(jsonHealthCheck.GetPropertyValue(\"type\", \"Tcp\"), true);\n            _interval = jsonHealthCheck.GetPropertyValue(\"interval\", 60) * 1000;\n            _retries = jsonHealthCheck.GetPropertyValue(\"retries\", 3);\n            _timeout = jsonHealthCheck.GetPropertyValue(\"timeout\", 10) * 1000;\n            _port = jsonHealthCheck.GetPropertyValue(\"port\", 80);\n\n            if (jsonHealthCheck.TryGetProperty(\"url\", out JsonElement jsonUrl) && (jsonUrl.ValueKind != JsonValueKind.Null))\n                _url = new Uri(jsonUrl.GetString());\n            else\n                _url = null;\n\n            if (jsonHealthCheck.TryGetProperty(\"emailAlert\", out JsonElement jsonEmailAlert) && _service.EmailAlerts.TryGetValue(jsonEmailAlert.GetString(), out EmailAlert emailAlert))\n                _emailAlert = emailAlert;\n            else\n                _emailAlert = null;\n\n            if (jsonHealthCheck.TryGetProperty(\"webHook\", out JsonElement jsonWebHook) && _service.WebHooks.TryGetValue(jsonWebHook.GetString(), out WebHook webHook))\n                _webHook = webHook;\n            else\n                _webHook = null;\n\n            ConditionalHttpReload();\n        }\n\n        public async Task<HealthCheckResponse> IsHealthyAsync(string domain, DnsResourceRecordType type, Uri healthCheckUrl)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.A:\n                    {\n                        DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));\n                        if ((response is null) || (response.Answer.Count == 0))\n                            return new HealthCheckResponse(HealthStatus.Failed, \"Failed to resolve address.\");\n\n                        IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseA(response);\n                        if (addresses.Count > 0)\n                        {\n                            HealthCheckResponse lastResponse = null;\n\n                            foreach (IPAddress address in addresses)\n                            {\n                                lastResponse = await IsHealthyAsync(address, healthCheckUrl);\n                                if (lastResponse.Status == HealthStatus.Healthy)\n                                    return lastResponse;\n                            }\n\n                            return lastResponse;\n                        }\n\n                        return new HealthCheckResponse(HealthStatus.Failed, \"Failed to resolve address.\");\n                    }\n\n                case DnsResourceRecordType.AAAA:\n                    {\n                        DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));\n                        if ((response is null) || (response.Answer.Count == 0))\n                            return new HealthCheckResponse(HealthStatus.Failed, \"Failed to resolve address.\");\n\n                        IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseAAAA(response);\n                        if (addresses.Count > 0)\n                        {\n                            HealthCheckResponse lastResponse = null;\n\n                            foreach (IPAddress address in addresses)\n                            {\n                                lastResponse = await IsHealthyAsync(address, healthCheckUrl);\n                                if (lastResponse.Status == HealthStatus.Healthy)\n                                    return lastResponse;\n                            }\n\n                            return lastResponse;\n                        }\n\n                        return new HealthCheckResponse(HealthStatus.Failed, \"Failed to resolve address.\");\n                    }\n\n                default:\n                    return new HealthCheckResponse(HealthStatus.Failed, \"Not supported.\");\n            }\n        }\n\n        public async Task<HealthCheckResponse> IsHealthyAsync(IPAddress address, Uri healthCheckUrl)\n        {\n            foreach (KeyValuePair<NetworkAddress, bool> network in _service.UnderMaintenance)\n            {\n                if (network.Key.Contains(address))\n                {\n                    if (network.Value)\n                        return new HealthCheckResponse(HealthStatus.Maintenance);\n\n                    break;\n                }\n            }\n\n            switch (_type)\n            {\n                case HealthCheckType.Ping:\n                    {\n                        if (_service.DnsServer.Proxy != null)\n                            throw new NotSupportedException(\"Health check type 'ping' is not supported over proxy.\");\n\n                        using (Ping ping = new Ping())\n                        {\n                            string lastReason;\n                            int retry = 0;\n                            do\n                            {\n                                PingReply reply = await ping.SendPingAsync(address, _timeout);\n                                if (reply.Status == IPStatus.Success)\n                                    return new HealthCheckResponse(HealthStatus.Healthy);\n\n                                lastReason = reply.Status.ToString();\n                            }\n                            while (++retry < _retries);\n\n                            return new HealthCheckResponse(HealthStatus.Failed, lastReason);\n                        }\n                    }\n\n                case HealthCheckType.Tcp:\n                    {\n                        Exception lastException;\n                        string lastReason = null;\n                        int retry = 0;\n                        do\n                        {\n                            try\n                            {\n                                NetProxy proxy = _service.DnsServer.Proxy;\n\n                                if (proxy is null)\n                                {\n                                    using (Socket socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))\n                                    {\n                                        await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                                        {\n                                            return socket.ConnectAsync(address, _port, cancellationToken1).AsTask();\n                                        }, _timeout);\n                                    }\n                                }\n                                else\n                                {\n                                    using (Socket socket = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                                        {\n                                            return proxy.ConnectAsync(new IPEndPoint(address, _port), cancellationToken1);\n                                        }, _timeout))\n                                    {\n                                        //do nothing\n                                    }\n                                }\n\n                                return new HealthCheckResponse(HealthStatus.Healthy);\n                            }\n                            catch (TimeoutException ex)\n                            {\n                                lastReason = \"Connection timed out.\";\n                                lastException = ex;\n                            }\n                            catch (SocketException ex)\n                            {\n                                lastReason = ex.Message;\n                                lastException = ex;\n                            }\n                            catch (Exception ex)\n                            {\n                                lastException = ex;\n                            }\n                        }\n                        while (++retry < _retries);\n\n                        return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException);\n                    }\n\n                case HealthCheckType.Http:\n                case HealthCheckType.Https:\n                    {\n                        ConditionalHttpReload();\n\n                        Exception lastException;\n                        string lastReason = null;\n                        int retry = 0;\n                        do\n                        {\n                            try\n                            {\n                                Uri url;\n\n                                if (_url is null)\n                                    url = healthCheckUrl;\n                                else\n                                    url = _url;\n\n                                if (url is null)\n                                    return new HealthCheckResponse(HealthStatus.Failed, \"Missing health check URL in APP record as well as in app config.\");\n\n                                if (_type == HealthCheckType.Http)\n                                {\n                                    if (url.Scheme.Equals(\"https\", StringComparison.OrdinalIgnoreCase))\n                                        url = new Uri(\"http://\" + url.Host + (url.IsDefaultPort ? \"\" : \":\" + url.Port) + url.PathAndQuery);\n                                }\n                                else\n                                {\n                                    if (url.Scheme.Equals(\"http\", StringComparison.OrdinalIgnoreCase))\n                                        url = new Uri(\"https://\" + url.Host + (url.IsDefaultPort ? \"\" : \":\" + url.Port) + url.PathAndQuery);\n                                }\n\n                                IPEndPoint ep = new IPEndPoint(address, url.Port);\n                                Uri queryUri = new Uri(url.Scheme + \"://\" + ep.ToString() + url.PathAndQuery);\n                                HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, queryUri);\n\n                                if (url.IsDefaultPort)\n                                    httpRequest.Headers.Host = url.Host;\n                                else\n                                    httpRequest.Headers.Host = url.Host + \":\" + url.Port;\n\n                                HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest);\n                                if (httpResponse.IsSuccessStatusCode)\n                                    return new HealthCheckResponse(HealthStatus.Healthy);\n\n                                return new HealthCheckResponse(HealthStatus.Failed, \"Received HTTP status code: \" + (int)httpResponse.StatusCode + \" \" + httpResponse.StatusCode.ToString() + \"; URL: \" + url.AbsoluteUri);\n                            }\n                            catch (OperationCanceledException ex)\n                            {\n                                lastReason = \"Connection timed out.\";\n                                lastException = ex;\n                            }\n                            catch (HttpRequestException ex)\n                            {\n                                lastReason = ex.Message;\n                                lastException = ex;\n                            }\n                            catch (Exception ex)\n                            {\n                                lastException = ex;\n                            }\n                        }\n                        while (++retry < _retries);\n\n                        return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException);\n                    }\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Name\n        { get { return _name; } }\n\n        public HealthCheckType Type\n        { get { return _type; } }\n\n        public int Interval\n        { get { return _interval; } }\n\n        public int Retries\n        { get { return _retries; } }\n\n        public int Timeout\n        { get { return _timeout; } }\n\n        public int Port\n        { get { return _port; } }\n\n        public Uri Url\n        { get { return _url; } }\n\n        public EmailAlert EmailAlert\n        { get { return _emailAlert; } }\n\n        public WebHook WebHook\n        { get { return _webHook; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/HealthCheckResponse.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace Failover\n{\n    enum HealthStatus\n    {\n        Unknown = 0,\n        Failed = 1,\n        Healthy = 2,\n        Maintenance = 3\n    }\n\n    class HealthCheckResponse\n    {\n        #region variables\n\n        public readonly DateTime DateTime = DateTime.UtcNow;\n        public readonly HealthStatus Status;\n        public readonly string FailureReason;\n        public readonly Exception Exception;\n\n        #endregion\n\n        #region constructor\n\n        public HealthCheckResponse(HealthStatus status, string failureReason = null, Exception exception = null)\n        {\n            Status = status;\n            FailureReason = failureReason;\n            Exception = exception;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/HealthMonitor.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Net;\nusing System.Threading;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace Failover\n{\n    class HealthMonitor : IDisposable\n    {\n        #region variables\n\n        readonly IDnsServer _dnsServer;\n        readonly IPAddress _address;\n        readonly string _domain;\n        readonly DnsResourceRecordType _type;\n        readonly HealthCheck _healthCheck;\n\n        readonly Timer _healthCheckTimer;\n        const int HEALTH_CHECK_TIMER_INITIAL_INTERVAL = 1000;\n\n        HealthCheckResponse _lastHealthCheckResponse;\n\n        const int MONITOR_EXPIRY = 1 * 60 * 60 * 1000; //1 hour\n        DateTime _lastHealthStatusCheckedOn;\n\n        #endregion\n\n        #region constructor\n\n        public HealthMonitor(IDnsServer dnsServer, IPAddress address, HealthCheck healthCheck, Uri healthCheckUrl)\n        {\n            _dnsServer = dnsServer;\n            _address = address;\n            _healthCheck = healthCheck;\n\n            _healthCheckTimer = new Timer(async delegate (object state)\n            {\n                try\n                {\n                    if (_healthCheck is null)\n                    {\n                        _lastHealthCheckResponse = null;\n                    }\n                    else\n                    {\n                        HealthCheckResponse healthCheckResponse = await _healthCheck.IsHealthyAsync(_address, healthCheckUrl);\n\n                        bool statusChanged = false;\n                        bool maintenance = false;\n\n                        if (_lastHealthCheckResponse is null)\n                        {\n                            switch (healthCheckResponse.Status)\n                            {\n                                case HealthStatus.Failed:\n                                    statusChanged = true;\n                                    break;\n\n                                case HealthStatus.Maintenance:\n                                    statusChanged = true;\n                                    maintenance = true;\n                                    break;\n                            }\n                        }\n                        else\n                        {\n                            if (_lastHealthCheckResponse.Status != healthCheckResponse.Status)\n                            {\n                                statusChanged = true;\n\n                                if ((_lastHealthCheckResponse.Status == HealthStatus.Maintenance) || (healthCheckResponse.Status == HealthStatus.Maintenance))\n                                    maintenance = true;\n                            }\n                        }\n\n                        if (statusChanged)\n                        {\n                            switch (healthCheckResponse.Status)\n                            {\n                                case HealthStatus.Failed:\n                                    _dnsServer.WriteLog(\"ALERT! Address [\" + _address.ToString() + \"] status is FAILED based on '\" + _healthCheck.Name + \"' health check. The failure reason is: \" + healthCheckResponse.FailureReason);\n                                    break;\n\n                                default:\n                                    _dnsServer.WriteLog(\"ALERT! Address [\" + _address.ToString() + \"] status is \" + healthCheckResponse.Status.ToString().ToUpper() + \" based on '\" + _healthCheck.Name + \"' health check.\");\n                                    break;\n                            }\n\n                            if (healthCheckResponse.Exception is not null)\n                                _dnsServer.WriteLog(healthCheckResponse.Exception);\n\n                            if (!maintenance)\n                            {\n                                //avoid sending email alerts when switching from or to maintenance\n                                EmailAlert emailAlert = _healthCheck.EmailAlert;\n                                if (emailAlert is not null)\n                                    _ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, healthCheckResponse);\n                            }\n\n                            WebHook webHook = _healthCheck.WebHook;\n                            if (webHook is not null)\n                                _ = webHook.CallAsync(_address, _healthCheck.Name, healthCheckResponse);\n                        }\n\n                        _lastHealthCheckResponse = healthCheckResponse;\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(ex);\n\n                    if (_lastHealthCheckResponse is null)\n                    {\n                        EmailAlert emailAlert = _healthCheck.EmailAlert;\n                        if (emailAlert is not null)\n                            _ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, ex);\n\n                        WebHook webHook = _healthCheck.WebHook;\n                        if (webHook is not null)\n                            _ = webHook.CallAsync(_address, _healthCheck.Name, ex);\n\n                        _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Failed, ex.ToString(), ex);\n                    }\n                    else\n                    {\n                        _lastHealthCheckResponse = null;\n                    }\n                }\n                finally\n                {\n                    if (!_disposed && (_healthCheck is not null))\n                        _healthCheckTimer.Change(_healthCheck.Interval, Timeout.Infinite);\n                }\n            }, null, Timeout.Infinite, Timeout.Infinite);\n\n            _healthCheckTimer.Change(HEALTH_CHECK_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n        }\n\n        public HealthMonitor(IDnsServer dnsServer, string domain, DnsResourceRecordType type, HealthCheck healthCheck, Uri healthCheckUrl)\n        {\n            _dnsServer = dnsServer;\n            _domain = domain;\n            _type = type;\n            _healthCheck = healthCheck;\n\n            _healthCheckTimer = new Timer(async delegate (object state)\n            {\n                try\n                {\n                    if (_healthCheck is null)\n                    {\n                        _lastHealthCheckResponse = null;\n                    }\n                    else\n                    {\n                        HealthCheckResponse healthCheckResponse = await _healthCheck.IsHealthyAsync(_domain, _type, healthCheckUrl);\n\n                        bool statusChanged = false;\n                        bool maintenance = false;\n\n                        if (_lastHealthCheckResponse is null)\n                        {\n                            switch (healthCheckResponse.Status)\n                            {\n                                case HealthStatus.Failed:\n                                    statusChanged = true;\n                                    break;\n\n                                case HealthStatus.Maintenance:\n                                    statusChanged = true;\n                                    maintenance = true;\n                                    break;\n                            }\n                        }\n                        else\n                        {\n                            if (_lastHealthCheckResponse.Status != healthCheckResponse.Status)\n                            {\n                                statusChanged = true;\n\n                                if ((_lastHealthCheckResponse.Status == HealthStatus.Maintenance) || (healthCheckResponse.Status == HealthStatus.Maintenance))\n                                    maintenance = true;\n                            }\n                        }\n\n                        if (statusChanged)\n                        {\n                            switch (healthCheckResponse.Status)\n                            {\n                                case HealthStatus.Failed:\n                                    _dnsServer.WriteLog(\"ALERT! Domain [\" + _domain + \"] type [\" + _type.ToString() + \"] status is FAILED based on '\" + _healthCheck.Name + \"' health check. The failure reason is: \" + healthCheckResponse.FailureReason);\n                                    break;\n\n                                default:\n                                    _dnsServer.WriteLog(\"ALERT! Domain [\" + _domain + \"] type [\" + _type.ToString() + \"] status is \" + healthCheckResponse.Status.ToString().ToUpper() + \" based on '\" + _healthCheck.Name + \"' health check.\");\n                                    break;\n                            }\n\n                            if (healthCheckResponse.Exception is not null)\n                                _dnsServer.WriteLog(healthCheckResponse.Exception);\n\n                            if (!maintenance)\n                            {\n                                //avoid sending email alerts when switching from or to maintenance\n                                EmailAlert emailAlert = _healthCheck.EmailAlert;\n                                if (emailAlert is not null)\n                                    _ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, healthCheckResponse);\n                            }\n\n                            WebHook webHook = _healthCheck.WebHook;\n                            if (webHook is not null)\n                                _ = webHook.CallAsync(_domain, _type, _healthCheck.Name, healthCheckResponse);\n                        }\n\n                        _lastHealthCheckResponse = healthCheckResponse;\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(ex);\n\n                    if (_lastHealthCheckResponse is null)\n                    {\n                        EmailAlert emailAlert = _healthCheck.EmailAlert;\n                        if (emailAlert is not null)\n                            _ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, ex);\n\n                        WebHook webHook = _healthCheck.WebHook;\n                        if (webHook is not null)\n                            _ = webHook.CallAsync(_domain, _type, _healthCheck.Name, ex);\n\n                        _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Failed, ex.ToString(), ex);\n                    }\n                    else\n                    {\n                        _lastHealthCheckResponse = null;\n                    }\n                }\n                finally\n                {\n                    try\n                    {\n                        _healthCheckTimer?.Change(_healthCheck.Interval, Timeout.Infinite);\n                    }\n                    catch (ObjectDisposedException)\n                    { }\n                }\n            }, null, Timeout.Infinite, Timeout.Infinite);\n\n            _healthCheckTimer.Change(HEALTH_CHECK_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                _healthCheckTimer?.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region public\n\n        public bool IsExpired()\n        {\n            return DateTime.UtcNow > _lastHealthStatusCheckedOn.AddMilliseconds(MONITOR_EXPIRY);\n        }\n\n        public void SetUnderMaintenance()\n        {\n            _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Maintenance);\n        }\n\n        #endregion\n\n        #region properties\n\n        public IPAddress Address\n        { get { return _address; } }\n\n        public HealthCheckResponse LastHealthCheckResponse\n        {\n            get\n            {\n                _lastHealthStatusCheckedOn = DateTime.UtcNow;\n\n                if (_lastHealthCheckResponse is null)\n                    return new HealthCheckResponse(HealthStatus.Unknown);\n\n                return _lastHealthCheckResponse;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/HealthService.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace Failover\n{\n    class HealthService : IDisposable\n    {\n        #region variables\n\n        static HealthService _healthService;\n\n        readonly IDnsServer _dnsServer;\n\n        readonly ConcurrentDictionary<string, HealthCheck> _healthChecks = new ConcurrentDictionary<string, HealthCheck>(1, 5);\n        readonly ConcurrentDictionary<string, EmailAlert> _emailAlerts = new ConcurrentDictionary<string, EmailAlert>(1, 2);\n        readonly ConcurrentDictionary<string, WebHook> _webHooks = new ConcurrentDictionary<string, WebHook>(1, 2);\n        readonly ConcurrentDictionary<NetworkAddress, bool> _underMaintenance = new ConcurrentDictionary<NetworkAddress, bool>();\n\n        readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>();\n\n        readonly Timer _maintenanceTimer;\n        const int MAINTENANCE_TIMER_INTERVAL = 15 * 60 * 1000; //15 mins\n\n        #endregion\n\n        #region constructor\n\n        private HealthService(IDnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n\n            _maintenanceTimer = new Timer(delegate (object state)\n            {\n                try\n                {\n                    foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)\n                    {\n                        if (healthMonitor.Value.IsExpired())\n                        {\n                            if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))\n                                removedMonitor.Dispose();\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(ex);\n                }\n                finally\n                {\n                    try\n                    {\n                        _maintenanceTimer?.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);\n                    }\n                    catch (ObjectDisposedException)\n                    { }\n                }\n            }, null, Timeout.Infinite, Timeout.Infinite);\n\n            _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                _maintenanceTimer?.Dispose();\n\n                foreach (KeyValuePair<string, HealthCheck> healthCheck in _healthChecks)\n                    healthCheck.Value.Dispose();\n\n                _healthChecks.Clear();\n\n                foreach (KeyValuePair<string, EmailAlert> emailAlert in _emailAlerts)\n                    emailAlert.Value.Dispose();\n\n                _emailAlerts.Clear();\n\n                foreach (KeyValuePair<string, WebHook> webHook in _webHooks)\n                    webHook.Value.Dispose();\n\n                _webHooks.Clear();\n\n                foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)\n                    healthMonitor.Value.Dispose();\n\n                _healthMonitors.Clear();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region static\n\n        public static HealthService Create(IDnsServer dnsServer)\n        {\n            if (_healthService is null)\n                _healthService = new HealthService(dnsServer);\n\n            return _healthService;\n        }\n\n        #endregion\n\n        #region private\n\n        private static string GetHealthMonitorKey(IPAddress address, string healthCheck, Uri healthCheckUrl)\n        {\n            //key: health-check|127.0.0.1\n            //key: health-check|127.0.0.1|http://example.com/\n\n            if (healthCheckUrl is null)\n                return healthCheck + \"|\" + address.ToString();\n            else\n                return healthCheck + \"|\" + address.ToString() + \"|\" + healthCheckUrl.AbsoluteUri;\n        }\n\n        private static string GetHealthMonitorKey(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl)\n        {\n            //key: health-check|example.com|A\n            //key: health-check|example.com|AAAA|http://example.com/\n\n            if (healthCheckUrl is null)\n                return healthCheck + \"|\" + domain + \"|\" + type.ToString();\n            else\n                return healthCheck + \"|\" + domain + \"|\" + type.ToString() + \"|\" + healthCheckUrl.AbsoluteUri;\n        }\n\n        private void RemoveHealthMonitor(string healthCheck)\n        {\n            foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)\n            {\n                if (healthMonitor.Key.StartsWith(healthCheck + \"|\"))\n                {\n                    if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))\n                        removedMonitor.Dispose();\n                }\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void Initialize(string config)\n        {\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            //email alerts\n            {\n                JsonElement jsonEmailAlerts = jsonConfig.GetProperty(\"emailAlerts\");\n\n                //add or update email alerts\n                foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray())\n                {\n                    string name = jsonEmailAlert.GetPropertyValue(\"name\", \"default\");\n\n                    if (_emailAlerts.TryGetValue(name, out EmailAlert existingEmailAlert))\n                    {\n                        //update\n                        existingEmailAlert.Reload(jsonEmailAlert);\n                    }\n                    else\n                    {\n                        //add\n                        EmailAlert emailAlert = new EmailAlert(this, jsonEmailAlert);\n                        _emailAlerts.TryAdd(emailAlert.Name, emailAlert);\n                    }\n                }\n\n                //remove email alerts that dont exists in config\n                foreach (KeyValuePair<string, EmailAlert> emailAlert in _emailAlerts)\n                {\n                    bool emailAlertExists = false;\n\n                    foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray())\n                    {\n                        string name = jsonEmailAlert.GetPropertyValue(\"name\", \"default\");\n                        if (name == emailAlert.Key)\n                        {\n                            emailAlertExists = true;\n                            break;\n                        }\n                    }\n\n                    if (!emailAlertExists)\n                    {\n                        if (_emailAlerts.TryRemove(emailAlert.Key, out EmailAlert removedEmailAlert))\n                            removedEmailAlert.Dispose();\n                    }\n                }\n            }\n\n            //web hooks\n            {\n                JsonElement jsonWebHooks = jsonConfig.GetProperty(\"webHooks\");\n\n                //add or update email alerts\n                foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray())\n                {\n                    string name = jsonWebHook.GetPropertyValue(\"name\", \"default\");\n\n                    if (_webHooks.TryGetValue(name, out WebHook existingWebHook))\n                    {\n                        //update\n                        existingWebHook.Reload(jsonWebHook);\n                    }\n                    else\n                    {\n                        //add\n                        WebHook webHook = new WebHook(this, jsonWebHook);\n                        _webHooks.TryAdd(webHook.Name, webHook);\n                    }\n                }\n\n                //remove email alerts that dont exists in config\n                foreach (KeyValuePair<string, WebHook> webHook in _webHooks)\n                {\n                    bool webHookExists = false;\n\n                    foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray())\n                    {\n                        string name = jsonWebHook.GetPropertyValue(\"name\", \"default\");\n                        if (name == webHook.Key)\n                        {\n                            webHookExists = true;\n                            break;\n                        }\n                    }\n\n                    if (!webHookExists)\n                    {\n                        if (_webHooks.TryRemove(webHook.Key, out WebHook removedWebHook))\n                            removedWebHook.Dispose();\n                    }\n                }\n            }\n\n            //health checks\n            {\n                JsonElement jsonHealthChecks = jsonConfig.GetProperty(\"healthChecks\");\n\n                //add or update health checks\n                foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray())\n                {\n                    string name = jsonHealthCheck.GetPropertyValue(\"name\", \"default\");\n\n                    if (_healthChecks.TryGetValue(name, out HealthCheck existingHealthCheck))\n                    {\n                        //update\n                        existingHealthCheck.Reload(jsonHealthCheck);\n                    }\n                    else\n                    {\n                        //add\n                        HealthCheck healthCheck = new HealthCheck(this, jsonHealthCheck);\n                        _healthChecks.TryAdd(healthCheck.Name, healthCheck);\n                    }\n                }\n\n                //remove health checks that dont exists in config\n                foreach (KeyValuePair<string, HealthCheck> healthCheck in _healthChecks)\n                {\n                    bool healthCheckExists = false;\n\n                    foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray())\n                    {\n                        string name = jsonHealthCheck.GetPropertyValue(\"name\", \"default\");\n                        if (name == healthCheck.Key)\n                        {\n                            healthCheckExists = true;\n                            break;\n                        }\n                    }\n\n                    if (!healthCheckExists)\n                    {\n                        if (_healthChecks.TryRemove(healthCheck.Key, out HealthCheck removedHealthCheck))\n                        {\n                            //remove health monitors using this health check\n                            RemoveHealthMonitor(healthCheck.Key);\n\n                            removedHealthCheck.Dispose();\n                        }\n                    }\n                }\n            }\n\n            //under maintenance networks\n            _underMaintenance.Clear();\n\n            if (jsonConfig.TryGetProperty(\"underMaintenance\", out JsonElement jsonUnderMaintenance))\n            {\n                foreach (JsonElement jsonNetwork in jsonUnderMaintenance.EnumerateArray())\n                {\n                    string network = jsonNetwork.GetProperty(\"network\").GetString();\n                    bool enabled;\n\n                    if (jsonNetwork.TryGetProperty(\"enabled\", out JsonElement jsonEnabled))\n                        enabled = jsonEnabled.GetBoolean();\n                    else if (jsonNetwork.TryGetProperty(\"enable\", out JsonElement jsonEnable))\n                        enabled = jsonEnable.GetBoolean();\n                    else\n                        enabled = true;\n\n                    NetworkAddress umNetwork = NetworkAddress.Parse(network);\n\n                    if (_underMaintenance.TryAdd(umNetwork, enabled))\n                    {\n                        if (enabled)\n                        {\n                            foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)\n                            {\n                                HealthMonitor monitor = healthMonitor.Value;\n\n                                if (monitor.Address is null)\n                                    continue;\n\n                                if (umNetwork.Contains(monitor.Address))\n                                    monitor.SetUnderMaintenance();\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        public HealthCheckResponse QueryStatus(IPAddress address, string healthCheck, Uri healthCheckUrl, bool tryAdd)\n        {\n            string healthMonitorKey = GetHealthMonitorKey(address, healthCheck, healthCheckUrl);\n\n            if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor))\n                return monitor.LastHealthCheckResponse;\n\n            if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))\n            {\n                if (tryAdd)\n                {\n                    monitor = new HealthMonitor(_dnsServer, address, existingHealthCheck, healthCheckUrl);\n\n                    if (!_healthMonitors.TryAdd(healthMonitorKey, monitor))\n                        monitor.Dispose(); //failed to add first\n                }\n\n                return new HealthCheckResponse(HealthStatus.Unknown);\n            }\n            else\n            {\n                return new HealthCheckResponse(HealthStatus.Failed, \"No such health check: \" + healthCheck);\n            }\n        }\n\n        public HealthCheckResponse QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl, bool tryAdd)\n        {\n            domain = domain.ToLowerInvariant();\n\n            string healthMonitorKey = GetHealthMonitorKey(domain, type, healthCheck, healthCheckUrl);\n\n            if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor))\n                return monitor.LastHealthCheckResponse;\n\n            if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))\n            {\n                if (tryAdd)\n                {\n                    monitor = new HealthMonitor(_dnsServer, domain, type, existingHealthCheck, healthCheckUrl);\n\n                    if (!_healthMonitors.TryAdd(healthMonitorKey, monitor))\n                        monitor.Dispose(); //failed to add first\n                }\n\n                return new HealthCheckResponse(HealthStatus.Unknown);\n            }\n            else\n            {\n                return new HealthCheckResponse(HealthStatus.Failed, \"No such health check: \" + healthCheck);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public ConcurrentDictionary<string, HealthCheck> HealthChecks\n        { get { return _healthChecks; } }\n\n        public ConcurrentDictionary<string, EmailAlert> EmailAlerts\n        { get { return _emailAlerts; } }\n\n        public ConcurrentDictionary<string, WebHook> WebHooks\n        { get { return _webHooks; } }\n\n        public ConcurrentDictionary<NetworkAddress, bool> UnderMaintenance\n        { get { return _underMaintenance; } }\n\n        public IDnsServer DnsServer\n        { get { return _dnsServer; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/WebHook.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Http.Client;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace Failover\n{\n    class WebHook : IDisposable\n    {\n        #region variables\n\n        readonly HealthService _service;\n\n        readonly string _name;\n        bool _enabled;\n        Uri[] _urls;\n\n        HttpClientNetworkHandler _httpHandler;\n        HttpClient _httpClient;\n\n        #endregion\n\n        #region constructor\n\n        public WebHook(HealthService service, JsonElement jsonWebHook)\n        {\n            _service = service;\n\n            _name = jsonWebHook.GetPropertyValue(\"name\", \"default\");\n\n            Reload(jsonWebHook);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_httpClient != null)\n                {\n                    _httpClient.Dispose();\n                    _httpClient = null;\n                }\n\n                if (_httpHandler != null)\n                {\n                    _httpHandler.Dispose();\n                    _httpHandler = null;\n                }\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private void ConditionalHttpReload()\n        {\n            bool handlerChanged = false;\n            NetProxy proxy = _service.DnsServer.Proxy;\n\n            if (_httpHandler is null)\n            {\n                HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler();\n                httpHandler.Proxy = proxy;\n                httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                httpHandler.DnsClient = _service.DnsServer;\n\n                httpHandler.InnerHandler.AllowAutoRedirect = true;\n                httpHandler.InnerHandler.MaxAutomaticRedirections = 10;\n\n                _httpHandler = httpHandler;\n                handlerChanged = true;\n            }\n            else\n            {\n                if (_httpHandler.Proxy != proxy)\n                {\n                    HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler();\n                    httpHandler.Proxy = proxy;\n                    httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                    httpHandler.DnsClient = _service.DnsServer;\n\n                    httpHandler.InnerHandler.AllowAutoRedirect = true;\n                    httpHandler.InnerHandler.MaxAutomaticRedirections = 10;\n\n                    HttpClientNetworkHandler oldHttpHandler = _httpHandler;\n                    _httpHandler = httpHandler;\n                    handlerChanged = true;\n\n                    oldHttpHandler.Dispose();\n                }\n            }\n\n            if (_httpClient is null)\n            {\n                HttpClient httpClient = new HttpClient(_httpHandler);\n\n                _httpClient = httpClient;\n            }\n            else\n            {\n                if (handlerChanged)\n                {\n                    HttpClient httpClient = new HttpClient(_httpHandler);\n\n                    HttpClient oldHttpClient = _httpClient;\n                    _httpClient = httpClient;\n\n                    oldHttpClient.Dispose();\n                }\n            }\n        }\n\n        private async Task CallAsync(HttpContent content)\n        {\n            ConditionalHttpReload();\n\n            async Task CallWebHook(Uri url)\n            {\n                try\n                {\n                    HttpResponseMessage response = await _httpClient.PostAsync(url, content);\n                    response.EnsureSuccessStatusCode();\n                }\n                catch (Exception ex)\n                {\n                    _service.DnsServer.WriteLog(\"Webhook call failed for URL: \" + url.AbsoluteUri + \"\\r\\n\" + ex.ToString());\n                }\n            }\n\n            List<Task> tasks = new List<Task>();\n\n            foreach (Uri url in _urls)\n                tasks.Add(CallWebHook(url));\n\n            await Task.WhenAll(tasks);\n        }\n\n        #endregion\n\n        #region public\n\n        public void Reload(JsonElement jsonWebHook)\n        {\n            _enabled = jsonWebHook.GetPropertyValue(\"enabled\", false);\n\n            if (jsonWebHook.TryReadArray(\"urls\", delegate (string uri) { return new Uri(uri); }, out Uri[] urls))\n                _urls = urls;\n            else\n                _urls = null;\n\n            ConditionalHttpReload();\n        }\n\n        public Task CallAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse)\n        {\n            if (!_enabled)\n                return Task.CompletedTask;\n\n            HttpContent content;\n            {\n                using (MemoryStream mS = new MemoryStream())\n                {\n                    Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS);\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"address\", address.ToString());\n                    jsonWriter.WriteString(\"healthCheck\", healthCheck);\n                    jsonWriter.WriteString(\"status\", healthCheckResponse.Status.ToString());\n\n                    if (healthCheckResponse.Status == HealthStatus.Failed)\n                        jsonWriter.WriteString(\"failureReason\", healthCheckResponse.FailureReason);\n\n                    jsonWriter.WriteString(\"dateTime\", healthCheckResponse.DateTime);\n\n                    jsonWriter.WriteEndObject();\n                    jsonWriter.Flush();\n\n                    content = new ByteArrayContent(mS.ToArray());\n                    content.Headers.ContentType = new MediaTypeHeaderValue(\"application/json\");\n                }\n            }\n\n            return CallAsync(content);\n        }\n\n        public Task CallAsync(IPAddress address, string healthCheck, Exception ex)\n        {\n            if (!_enabled)\n                return Task.CompletedTask;\n\n            HttpContent content;\n            {\n                using (MemoryStream mS = new MemoryStream())\n                {\n                    Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS);\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"address\", address.ToString());\n                    jsonWriter.WriteString(\"healthCheck\", healthCheck);\n                    jsonWriter.WriteString(\"status\", \"Error\");\n                    jsonWriter.WriteString(\"failureReason\", ex.ToString());\n                    jsonWriter.WriteString(\"dateTime\", DateTime.UtcNow);\n\n                    jsonWriter.WriteEndObject();\n                    jsonWriter.Flush();\n\n                    content = new ByteArrayContent(mS.ToArray());\n                    content.Headers.ContentType = new MediaTypeHeaderValue(\"application/json\");\n                }\n            }\n\n            return CallAsync(content);\n        }\n\n        public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse)\n        {\n            if (!_enabled)\n                return Task.CompletedTask;\n\n            HttpContent content;\n            {\n                using (MemoryStream mS = new MemoryStream())\n                {\n                    Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS);\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"domain\", domain);\n                    jsonWriter.WriteString(\"recordType\", type.ToString());\n                    jsonWriter.WriteString(\"healthCheck\", healthCheck);\n                    jsonWriter.WriteString(\"status\", healthCheckResponse.Status.ToString());\n\n                    if (healthCheckResponse.Status == HealthStatus.Failed)\n                        jsonWriter.WriteString(\"failureReason\", healthCheckResponse.FailureReason);\n\n                    jsonWriter.WriteString(\"dateTime\", healthCheckResponse.DateTime);\n\n                    jsonWriter.WriteEndObject();\n                    jsonWriter.Flush();\n\n                    content = new ByteArrayContent(mS.ToArray());\n                    content.Headers.ContentType = new MediaTypeHeaderValue(\"application/json\");\n                }\n            }\n\n            return CallAsync(content);\n        }\n\n        public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)\n        {\n            if (!_enabled)\n                return Task.CompletedTask;\n\n            HttpContent content;\n            {\n                using (MemoryStream mS = new MemoryStream())\n                {\n                    Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS);\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"domain\", domain);\n                    jsonWriter.WriteString(\"recordType\", type.ToString());\n                    jsonWriter.WriteString(\"healthCheck\", healthCheck);\n                    jsonWriter.WriteString(\"status\", \"Error\");\n                    jsonWriter.WriteString(\"failureReason\", ex.ToString());\n                    jsonWriter.WriteString(\"dateTime\", DateTime.UtcNow);\n\n                    jsonWriter.WriteEndObject();\n                    jsonWriter.Flush();\n\n                    content = new ByteArrayContent(mS.ToArray());\n                    content.Headers.ContentType = new MediaTypeHeaderValue(\"application/json\");\n                }\n            }\n\n            return CallAsync(content);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Name\n        { get { return _name; } }\n\n        public bool Enabled\n        { get { return _enabled; } }\n\n        public Uri[] Urls\n        { get { return _urls; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FailoverApp/dnsApp.config",
    "content": "{\n  \"healthChecks\": [\n    {\n      \"name\": \"ping\",\n      \"type\": \"ping\",\n      \"interval\": 60,\n      \"retries\": 3,\n      \"timeout\": 10,\n      \"emailAlert\": \"default\",\n      \"webHook\": \"default\"\n    },\n    {\n      \"name\": \"tcp80\",\n      \"type\": \"tcp\",\n      \"interval\": 60,\n      \"retries\": 3,\n      \"timeout\": 10,\n      \"port\": 80,\n      \"emailAlert\": \"default\",\n      \"webHook\": \"default\"\n    },\n    {\n      \"name\": \"tcp443\",\n      \"type\": \"tcp\",\n      \"interval\": 60,\n      \"retries\": 3,\n      \"timeout\": 10,\n      \"port\": 443,\n      \"emailAlert\": \"default\",\n      \"webHook\": \"default\"\n    },\n    {\n      \"name\": \"http\",\n      \"type\": \"http\",\n      \"interval\": 60,\n      \"retries\": 3,\n      \"timeout\": 10,\n      \"url\": null,\n      \"emailAlert\": \"default\",\n      \"webHook\": \"default\"\n    },\n    {\n      \"name\": \"https\",\n      \"type\": \"https\",\n      \"interval\": 60,\n      \"retries\": 3,\n      \"timeout\": 10,\n      \"url\": null,\n      \"emailAlert\": \"default\",\n      \"webHook\": \"default\"\n    },\n    {\n      \"name\": \"www.example.com\",\n      \"type\": \"https\",\n      \"interval\": 60,\n      \"retries\": 3,\n      \"timeout\": 10,\n      \"url\": \"https://www.example.com\",\n      \"emailAlert\": \"default\",\n      \"webHook\": \"default\"\n    }\n  ],\n  \"emailAlerts\": [\n    {\n      \"name\": \"default\",\n      \"enabled\": false,\n      \"alertTo\": [\n        \"admin@example.com\"\n      ],\n      \"smtpServer\": \"smtp.example.com\",\n      \"smtpPort\": 465,\n      \"startTls\": false,\n      \"smtpOverTls\": true,\n      \"username\": \"alerts@example.com\",\n      \"password\": \"password\",\n      \"mailFrom\": \"alerts@example.com\",\n      \"mailFromName\": \"DNS Server Alert\"\n    }\n  ],\n  \"webHooks\": [\n    {\n      \"name\": \"default\",\n      \"enabled\": false,\n      \"urls\": [\n        \"https://webhooks.example.com/default\"\n      ]\n    }\n  ],\n  \"underMaintenance\": [\n    {\n      \"network\": \"192.168.10.2/32\",\n      \"enabled\": false\n    },\n    {\n      \"network\": \"10.1.1.0/24\",\n      \"enabled\": false\n    }\n  ]\n}"
  },
  {
    "path": "Apps/FilterAaaaApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace FilterAaaa\n{\n    public sealed class App : IDnsApplication, IDnsPostProcessor\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n\n        bool _enableFilterAaaa;\n        uint _defaultTtl;\n        bool _bypassLocalZones;\n        NetworkAddress[] _bypassNetworks;\n        string[] _bypassDomains;\n        string[] _filterDomains;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _enableFilterAaaa = jsonConfig.GetPropertyValue(\"enableFilterAaaa\", false);\n\n            if (jsonConfig.TryGetProperty(\"defaultTtl\", out JsonElement jsonValue))\n            {\n                if (!jsonValue.TryGetUInt32(out _defaultTtl))\n                    _defaultTtl = 30u;\n            }\n            else\n            {\n                _defaultTtl = 30u;\n\n                //update config for new option\n                config = config.Replace(\"\\\"bypassLocalZones\\\"\", \"\\\"defaultTtl\\\": 30,\\r\\n  \\\"bypassLocalZones\\\"\");\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n\n            _bypassLocalZones = jsonConfig.GetPropertyValue(\"bypassLocalZones\", false);\n\n            if (jsonConfig.TryReadArray(\"bypassNetworks\", NetworkAddress.Parse, out NetworkAddress[] bypassNetworks))\n                _bypassNetworks = bypassNetworks;\n            else\n                _bypassNetworks = [];\n\n            if (jsonConfig.TryReadArray(\"bypassDomains\", out string[] bypassDomains))\n                _bypassDomains = bypassDomains;\n            else\n                _bypassDomains = [];\n\n            if (jsonConfig.TryReadArray(\"filterDomains\", out string[] filterDomains))\n            {\n                _filterDomains = filterDomains;\n            }\n            else\n            {\n                _filterDomains = [];\n\n                //update config for new feature\n                config = config.TrimEnd('\\r', '\\n', ' ', '}');\n                config += \",\\r\\n  \\\"filterDomains\\\": [\\r\\n  ]\\r\\n}\";\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n        }\n\n        public async Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (!_enableFilterAaaa)\n                return response;\n\n            if (_bypassLocalZones && response.AuthoritativeAnswer)\n                return response;\n\n            if (response.RCODE != DnsResponseCode.NoError)\n                return response;\n\n            DnsQuestionRecord question = request.Question[0];\n            if (question.Type != DnsResourceRecordType.AAAA)\n                return response;\n\n            bool hasAAAA = false;\n\n            if (request.DnssecOk)\n            {\n                foreach (DnsResourceRecord record in response.Answer)\n                {\n                    switch (record.Type)\n                    {\n                        case DnsResourceRecordType.AAAA:\n                            hasAAAA = true;\n                            break;\n\n                        case DnsResourceRecordType.RRSIG:\n                            //response is signed and the client is DNSSEC aware; must not be modified\n                            return response;\n                    }\n                }\n            }\n            else\n            {\n                foreach (DnsResourceRecord record in response.Answer)\n                {\n                    if (record.Type == DnsResourceRecordType.AAAA)\n                    {\n                        hasAAAA = true;\n                        break;\n                    }\n                }\n            }\n\n            if (!hasAAAA)\n                return response;\n\n            IPAddress remoteIP = remoteEP.Address;\n\n            foreach (NetworkAddress network in _bypassNetworks)\n            {\n                if (network.Contains(remoteIP))\n                    return response;\n            }\n\n            string qname = question.Name;\n\n            foreach (string allowedDomain in _bypassDomains)\n            {\n                if (qname.Equals(allowedDomain, StringComparison.OrdinalIgnoreCase) || qname.EndsWith(\".\" + allowedDomain, StringComparison.OrdinalIgnoreCase))\n                    return response;\n            }\n\n            bool filterDomain = _filterDomains.Length == 0;\n\n            foreach (string blockedDomain in _filterDomains)\n            {\n                if (qname.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase) || qname.EndsWith(\".\" + blockedDomain, StringComparison.OrdinalIgnoreCase))\n                {\n                    filterDomain = true;\n                    break;\n                }\n            }\n\n            if (!filterDomain)\n                return response;\n\n            DnsDatagram aResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(qname, DnsResourceRecordType.A, DnsClass.IN), 2000);\n\n            if (aResponse.RCODE != DnsResponseCode.NoError)\n                return response;\n\n            foreach (DnsResourceRecord record in aResponse.Answer)\n            {\n                if (record.Type == DnsResourceRecordType.A)\n                {\n                    //domain has an A record; filter current AAAA response\n                    List<DnsResourceRecord> answer = new List<DnsResourceRecord>();\n\n                    foreach (DnsResourceRecord record2 in response.Answer)\n                    {\n                        if (record2.Type == DnsResourceRecordType.CNAME)\n                        {\n                            answer.Add(record2);\n                            qname = (record2.RDATA as DnsCNAMERecordData).Domain;\n                        }\n                    }\n\n                    DnsResourceRecord[] authority = [new DnsResourceRecord(qname, DnsResourceRecordType.SOA, DnsClass.IN, _defaultTtl, new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 3600, 900, 86400, _defaultTtl))];\n\n                    return new DnsDatagram(response.Identifier, true, response.OPCODE, false, false, response.RecursionDesired, response.RecursionAvailable, false, false, DnsResponseCode.NoError, response.Question, answer, authority);\n                }\n            }\n\n            //domain does not have an A record; return current response\n            return response;\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Filters AAAA records by returning NO DATA response when A records for the same domain name are available.\"; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/FilterAaaaApp/FilterAaaaApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>FilterAaaaApp</AssemblyName>\n    <RootNamespace>FilterAaaa</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <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.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/FilterAaaaApp/README.md",
    "content": "# Filter AAAA\n\nThe `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.\n\nThe app is a _post processor_. That means, it modifies a response generated by the DNS server before it is sent to the client.\n\n## Configuration\n\nAs any post processor, this app is configured globally in the app settings. Its configuration file is a JSON document which looks like the following:\n\n```\n{\n  \"enableFilterAaaa\": true,\n  \"defaultTtl\": 30,\n  \"bypassLocalZones\": false,\n  \"bypassNetworks\": [\n    \"192.168.1.0/24\"\n  ],\n  \"bypassDomains\": [\n    \"example.com\"\n  ],\n  \"filterDomains\": [\n  ]\n}\n```\n\nThe individual settings are:\n\n- `enableFilterAaaa`: when set to `false`, this app is disabled and passes through the original response.\n\n- `defaultTtl`: The default TTL (seconds) to use for the response. This will be used by clients to cache negative response.\n\n- `bypassLocalZones`: when set to `true`, authoritative answers are passed through unmodified.\n\n- `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.\n\n- `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`.\n\n- `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.\n\n## Post-processing\n\nThe app processes any response which matches all of the following criteria:\n\n- the response has a `NoError` response code\n- the query type is `AAAA`\n- the response contains at least one `AAAA` record\n- the request / response pair is not excluded by any configuration setting\n- a lookup for an up `A` record for the same domain is successful and returns an address\n\nNote that this means that `NXDOMAIN`, `SERVFAIL`, and `NODATA` responses are left unmodified.\n\nThe matching responses are replaced by one which includes all the `CNAME` records from the original response and a `SOA` record, but no `AAAA` record.\n"
  },
  {
    "path": "Apps/FilterAaaaApp/dnsApp.config",
    "content": "{\n  \"enableFilterAaaa\": true,\n  \"defaultTtl\": 30,\n  \"bypassLocalZones\": false,\n  \"bypassNetworks\": [\n  ],\n  \"bypassDomains\": [\n    \"example.com\"\n  ],\n  \"filterDomains\": [\n  ]\n}"
  },
  {
    "path": "Apps/GeoContinentApp/Address.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2.Responses;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace GeoContinent\n{\n    public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n        MaxMind _maxMind;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_maxMind is not null)\n                    _maxMind.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _maxMind = MaxMind.Create(dnsServer);\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))\n                    {\n                        JsonElement jsonAppRecordData = jsonDocument.RootElement;\n                        JsonElement jsonContinent = default;\n\n                        byte scopePrefixLength = 0;\n                        EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                        if (requestECS is not null)\n                        {\n                            if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null))\n                                scopePrefixLength = (byte)csIsp.Network.PrefixLength;\n                            else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null))\n                                scopePrefixLength = (byte)csAsn.Network.PrefixLength;\n                            else\n                                scopePrefixLength = requestECS.SourcePrefixLength;\n\n                            if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse))\n                            {\n                                if (!jsonAppRecordData.TryGetProperty(csResponse.Continent.Code, out jsonContinent))\n                                    jsonAppRecordData.TryGetProperty(\"default\", out jsonContinent);\n                            }\n                        }\n\n                        if (jsonContinent.ValueKind == JsonValueKind.Undefined)\n                        {\n                            if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response))\n                            {\n                                if (!jsonAppRecordData.TryGetProperty(response.Continent.Code, out jsonContinent))\n                                    jsonAppRecordData.TryGetProperty(\"default\", out jsonContinent);\n                            }\n                            else\n                            {\n                                jsonAppRecordData.TryGetProperty(\"default\", out jsonContinent);\n                            }\n\n                            if (jsonContinent.ValueKind == JsonValueKind.Undefined)\n                                return Task.FromResult<DnsDatagram>(null);\n                        }\n\n                        List<DnsResourceRecord> answers = new List<DnsResourceRecord>();\n\n                        switch (question.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                                foreach (JsonElement jsonAddress in jsonContinent.EnumerateArray())\n                                {\n                                    IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                    if (address.AddressFamily == AddressFamily.InterNetwork)\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address)));\n                                }\n                                break;\n\n                            case DnsResourceRecordType.AAAA:\n                                foreach (JsonElement jsonAddress in jsonContinent.EnumerateArray())\n                                {\n                                    IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                    if (address.AddressFamily == AddressFamily.InterNetworkV6)\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)));\n                                }\n                                break;\n                        }\n\n                        if (answers.Count == 0)\n                            return Task.FromResult<DnsDatagram>(null);\n\n                        if (answers.Count > 1)\n                            answers.Shuffle();\n\n                        EDnsOption[] options = null;\n\n                        if (requestECS is not null)\n                            options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address);\n\n                        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));\n                    }\n\n                default:\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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).\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"EU\"\": [\n    \"\"1.1.1.1\"\", \n    \"\"2.2.2.2\"\"\n  ],\n  \"\"default\"\": [\n    \"\"3.3.3.3\"\"\n  ]\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoContinentApp/CNAME.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2.Responses;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace GeoContinent\n{\n    public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n        MaxMind _maxMind;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_maxMind is not null)\n                    _maxMind.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _maxMind = MaxMind.Create(dnsServer);\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n            JsonElement jsonAppRecordData = jsonDocument.RootElement;\n            JsonElement jsonContinent = default;\n            string continentCode = null;\n\n            byte scopePrefixLength = 0;\n            EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n            if (requestECS is not null)\n            {\n                if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null))\n                    scopePrefixLength = (byte)csIsp.Network.PrefixLength;\n                else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null))\n                    scopePrefixLength = (byte)csAsn.Network.PrefixLength;\n                else\n                    scopePrefixLength = requestECS.SourcePrefixLength;\n\n                if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse))\n                {\n                    string cc = csResponse.Continent.Code;\n\n                    if (!jsonAppRecordData.TryGetProperty(cc, out jsonContinent))\n                    {\n                        jsonAppRecordData.TryGetProperty(\"default\", out jsonContinent);\n                        continentCode = cc is null ? \"default\" : cc.ToLowerInvariant();\n                    }\n                }\n            }\n\n            if (jsonContinent.ValueKind == JsonValueKind.Undefined)\n            {\n                if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response))\n                {\n                    string cc = response.Continent.Code;\n\n                    if (!jsonAppRecordData.TryGetProperty(cc, out jsonContinent))\n                    {\n                        jsonAppRecordData.TryGetProperty(\"default\", out jsonContinent);\n                        continentCode = cc is null ? \"default\" : cc.ToLowerInvariant();\n                    }\n                }\n                else\n                {\n                    jsonAppRecordData.TryGetProperty(\"default\", out jsonContinent);\n                    continentCode = \"default\";\n                }\n\n                if (jsonContinent.ValueKind == JsonValueKind.Undefined)\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n\n            string cname = jsonContinent.GetString();\n            if (string.IsNullOrEmpty(cname))\n                return Task.FromResult<DnsDatagram>(null);\n\n            if (continentCode is not null)\n                cname = cname.Replace(\"{ContinentCode}\", continentCode, StringComparison.OrdinalIgnoreCase);\n\n            IReadOnlyList<DnsResourceRecord> answers;\n\n            if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME\n            else\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) };\n\n            EDnsOption[] options = null;\n\n            if (requestECS is not null)\n                options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address);\n\n            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));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"EU\"\": \"\"eu.example.com\"\",\n  \"\"default\"\": \"\"example.com\"\"\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoContinentApp/GeoContinentApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<Version>9.0.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>GeoContinentApp</AssemblyName>\n\t\t<RootNamespace>GeoContinent</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"MaxMind.GeoIP2\" Version=\"5.4.1\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"ReadMe.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/GeoContinentApp/MaxMind.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2;\nusing System;\nusing System.IO;\n\nnamespace GeoContinent\n{\n    class MaxMind : IDisposable\n    {\n        #region variables\n\n        static MaxMind _maxMind;\n\n        readonly DatabaseReader _mmCountryReader;\n        readonly DatabaseReader _mmIspReader;\n        readonly DatabaseReader _mmAsnReader;\n\n        #endregion\n\n        #region constructor\n\n        private MaxMind(IDnsServer dnsServer)\n        {\n            string mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoIP2-Country.mmdb\");\n\n            if (!File.Exists(mmCountryFile))\n                mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoLite2-Country.mmdb\");\n\n            if (!File.Exists(mmCountryFile))\n                throw new FileNotFoundException(\"MaxMind Country file is missing!\");\n\n            _mmCountryReader = new DatabaseReader(mmCountryFile);\n\n            string mmIspFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoIP2-ISP.mmdb\");\n            if (File.Exists(mmIspFile))\n            {\n                _mmIspReader = new DatabaseReader(mmIspFile);\n                return;\n            }\n\n            string mmAsnFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoLite2-ASN.mmdb\");\n            if (File.Exists(mmAsnFile))\n                _mmAsnReader = new DatabaseReader(mmAsnFile);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                _mmCountryReader?.Dispose();\n                _mmIspReader?.Dispose();\n                _mmAsnReader?.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region public\n\n        public static MaxMind Create(IDnsServer dnsServer)\n        {\n            if (_maxMind is null)\n                _maxMind = new MaxMind(dnsServer);\n\n            return _maxMind;\n        }\n\n        #endregion\n\n        #region properties\n\n        public DatabaseReader CountryReader\n        { get { return _mmCountryReader; } }\n\n        public DatabaseReader IspReader\n        { get { return _mmIspReader; } }\n\n        public DatabaseReader AsnReader\n        { get { return _mmAsnReader; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoContinentApp/ReadMe.txt",
    "content": "﻿Using MaxMind GeoIP2 Database\n=============================\n\nThis app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial.\n\nFor production usage, it is required that you purchase the GeoIP2 database from MaxMind (https://www.maxmind.com/) and use it. \n\nTo 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.\n\nThe app optionally also uses MaxMind ISP/ASN database which can be updated the with same method as above."
  },
  {
    "path": "Apps/GeoContinentApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/GeoCountryApp/Address.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2.Responses;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace GeoCountry\n{\n    public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n        MaxMind _maxMind;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_maxMind is not null)\n                    _maxMind.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _maxMind = MaxMind.Create(dnsServer);\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))\n                    {\n                        JsonElement jsonAppRecordData = jsonDocument.RootElement;\n                        JsonElement jsonCountry = default;\n\n                        byte scopePrefixLength = 0;\n                        EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                        if (requestECS is not null)\n                        {\n                            if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null))\n                                scopePrefixLength = (byte)csIsp.Network.PrefixLength;\n                            else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null))\n                                scopePrefixLength = (byte)csAsn.Network.PrefixLength;\n                            else\n                                scopePrefixLength = requestECS.SourcePrefixLength;\n\n                            if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse))\n                            {\n                                if (!jsonAppRecordData.TryGetProperty(csResponse.Country.IsoCode, out jsonCountry))\n                                    jsonAppRecordData.TryGetProperty(\"default\", out jsonCountry);\n                            }\n                        }\n\n                        if (jsonCountry.ValueKind == JsonValueKind.Undefined)\n                        {\n                            if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response))\n                            {\n                                if (!jsonAppRecordData.TryGetProperty(response.Country.IsoCode, out jsonCountry))\n                                    jsonAppRecordData.TryGetProperty(\"default\", out jsonCountry);\n                            }\n                            else\n                            {\n                                jsonAppRecordData.TryGetProperty(\"default\", out jsonCountry);\n                            }\n\n                            if (jsonCountry.ValueKind == JsonValueKind.Undefined)\n                                return Task.FromResult<DnsDatagram>(null);\n                        }\n\n                        List<DnsResourceRecord> answers = new List<DnsResourceRecord>();\n\n                        switch (question.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                                foreach (JsonElement jsonAddress in jsonCountry.EnumerateArray())\n                                {\n                                    IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                    if (address.AddressFamily == AddressFamily.InterNetwork)\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address)));\n                                }\n                                break;\n\n                            case DnsResourceRecordType.AAAA:\n                                foreach (JsonElement jsonAddress in jsonCountry.EnumerateArray())\n                                {\n                                    IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                    if (address.AddressFamily == AddressFamily.InterNetworkV6)\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)));\n                                }\n                                break;\n                        }\n\n                        if (answers.Count == 0)\n                            return Task.FromResult<DnsDatagram>(null);\n\n                        if (answers.Count > 1)\n                            answers.Shuffle();\n\n                        EDnsOption[] options = null;\n\n                        if (requestECS is not null)\n                            options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address);\n\n                        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));\n                    }\n\n                default:\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"IN\"\": [\n    \"\"1.1.1.1\"\", \n    \"\"2.2.2.2\"\"\n  ],\n  \"\"default\"\": [\n    \"\"3.3.3.3\"\"\n  ]\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoCountryApp/CNAME.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2.Responses;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace GeoCountry\n{\n    public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n        MaxMind _maxMind;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_maxMind is not null)\n                    _maxMind.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _maxMind = MaxMind.Create(dnsServer);\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n            JsonElement jsonAppRecordData = jsonDocument.RootElement;\n            JsonElement jsonCountry = default;\n            string countryCode = null;\n\n            byte scopePrefixLength = 0;\n            EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n            if (requestECS is not null)\n            {\n                if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null))\n                    scopePrefixLength = (byte)csIsp.Network.PrefixLength;\n                else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null))\n                    scopePrefixLength = (byte)csAsn.Network.PrefixLength;\n                else\n                    scopePrefixLength = requestECS.SourcePrefixLength;\n\n                if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse))\n                {\n                    string cc = csResponse.Country.IsoCode;\n\n                    if (!jsonAppRecordData.TryGetProperty(cc, out jsonCountry))\n                    {\n                        jsonAppRecordData.TryGetProperty(\"default\", out jsonCountry);\n                        countryCode = cc is null ? \"default\" : cc.ToLowerInvariant();\n                    }\n                }\n            }\n\n            if (jsonCountry.ValueKind == JsonValueKind.Undefined)\n            {\n                if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response))\n                {\n                    string cc = response.Country.IsoCode;\n\n                    if (!jsonAppRecordData.TryGetProperty(cc, out jsonCountry))\n                    {\n                        jsonAppRecordData.TryGetProperty(\"default\", out jsonCountry);\n                        countryCode = cc is null ? \"default\" : cc.ToLowerInvariant();\n                    }\n                }\n                else\n                {\n                    jsonAppRecordData.TryGetProperty(\"default\", out jsonCountry);\n                    countryCode = \"default\";\n                }\n\n                if (jsonCountry.ValueKind == JsonValueKind.Undefined)\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n\n            string cname = jsonCountry.GetString();\n            if (string.IsNullOrEmpty(cname))\n                return Task.FromResult<DnsDatagram>(null);\n\n            if (countryCode is not null)\n                cname = cname.Replace(\"{CountryCode}\", countryCode, StringComparison.OrdinalIgnoreCase);\n\n            IReadOnlyList<DnsResourceRecord> answers;\n\n            if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME\n            else\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) };\n\n            EDnsOption[] options = null;\n\n            if (requestECS is not null)\n                options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address);\n\n            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));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"IN\"\": \"\"in.example.com\"\",\n  \"\"default\"\": \"\"example.com\"\"\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoCountryApp/GeoCountryApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<Version>9.0.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>GeoCountryApp</AssemblyName>\n\t\t<RootNamespace>GeoCountry</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"MaxMind.GeoIP2\" Version=\"5.4.1\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"ReadMe.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/GeoCountryApp/MaxMind.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2;\nusing System;\nusing System.IO;\n\nnamespace GeoCountry\n{\n    class MaxMind : IDisposable\n    {\n        #region variables\n\n        static MaxMind _maxMind;\n\n        readonly DatabaseReader _mmCountryReader;\n        readonly DatabaseReader _mmIspReader;\n        readonly DatabaseReader _mmAsnReader;\n\n        #endregion\n\n        #region constructor\n\n        private MaxMind(IDnsServer dnsServer)\n        {\n            string mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoIP2-Country.mmdb\");\n\n            if (!File.Exists(mmCountryFile))\n                mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoLite2-Country.mmdb\");\n\n            if (!File.Exists(mmCountryFile))\n                throw new FileNotFoundException(\"MaxMind Country file is missing!\");\n\n            _mmCountryReader = new DatabaseReader(mmCountryFile);\n\n            string mmIspFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoIP2-ISP.mmdb\");\n            if (File.Exists(mmIspFile))\n            {\n                _mmIspReader = new DatabaseReader(mmIspFile);\n                return;\n            }\n\n            string mmAsnFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoLite2-ASN.mmdb\");\n            if (File.Exists(mmAsnFile))\n                _mmAsnReader = new DatabaseReader(mmAsnFile);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                _mmCountryReader?.Dispose();\n                _mmIspReader?.Dispose();\n                _mmAsnReader?.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region public\n\n        public static MaxMind Create(IDnsServer dnsServer)\n        {\n            if (_maxMind is null)\n                _maxMind = new MaxMind(dnsServer);\n\n            return _maxMind;\n        }\n\n        #endregion\n\n        #region properties\n\n        public DatabaseReader CountryReader\n        { get { return _mmCountryReader; } }\n\n        public DatabaseReader IspReader\n        { get { return _mmIspReader; } }\n\n        public DatabaseReader AsnReader\n        { get { return _mmAsnReader; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoCountryApp/ReadMe.txt",
    "content": "﻿Using MaxMind GeoIP2 Database\n=============================\n\nThis app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial.\n\nFor production usage, it is required that you purchase the GeoIP2 database from MaxMind (https://www.maxmind.com/) and use it. \n\nTo 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.\n\nThe app optionally also uses MaxMind ISP/ASN database which can be updated the with same method as above."
  },
  {
    "path": "Apps/GeoCountryApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/GeoDistanceApp/Address.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2.Model;\nusing MaxMind.GeoIP2.Responses;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace GeoDistance\n{\n    public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n        MaxMind _maxMind;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_maxMind is not null)\n                    _maxMind.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region private\n\n        private static double GetDistance(double lat1, double long1, double lat2, double long2)\n        {\n            double d1 = lat1 * (Math.PI / 180.0);\n            double num1 = long1 * (Math.PI / 180.0);\n            double d2 = lat2 * (Math.PI / 180.0);\n            double num2 = long2 * (Math.PI / 180.0) - num1;\n            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);\n\n            return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _maxMind = MaxMind.Create(dnsServer);\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    Location location = null;\n\n                    byte scopePrefixLength = 0;\n                    EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                    if (requestECS is not null)\n                    {\n                        if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null))\n                            scopePrefixLength = (byte)csIsp.Network.PrefixLength;\n                        else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null))\n                            scopePrefixLength = (byte)csAsn.Network.PrefixLength;\n                        else\n                            scopePrefixLength = requestECS.SourcePrefixLength;\n\n                        if (_maxMind.CityReader.TryCity(requestECS.Address, out CityResponse csResponse) && csResponse.Location.HasCoordinates)\n                            location = csResponse.Location;\n                    }\n\n                    if ((location is null) && _maxMind.CityReader.TryCity(remoteEP.Address, out CityResponse response) && response.Location.HasCoordinates)\n                        location = response.Location;\n\n                    using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))\n                    {\n                        JsonElement jsonAppRecordData = jsonDocument.RootElement;\n                        JsonElement jsonClosestServer = default;\n\n                        if (location is null)\n                        {\n                            if (jsonAppRecordData.GetArrayLength() > 0)\n                                jsonClosestServer = jsonAppRecordData[0];\n                        }\n                        else\n                        {\n                            double lastDistance = double.MaxValue;\n\n                            foreach (JsonElement jsonServer in jsonAppRecordData.EnumerateArray())\n                            {\n                                double lat = Convert.ToDouble(jsonServer.GetProperty(\"lat\").GetString());\n                                double @long = Convert.ToDouble(jsonServer.GetProperty(\"long\").GetString());\n\n                                double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value);\n\n                                if (distance < lastDistance)\n                                {\n                                    lastDistance = distance;\n                                    jsonClosestServer = jsonServer;\n                                }\n                            }\n                        }\n\n                        if (jsonClosestServer.ValueKind == JsonValueKind.Undefined)\n                            return Task.FromResult<DnsDatagram>(null);\n\n                        List<DnsResourceRecord> answers = new List<DnsResourceRecord>();\n\n                        switch (question.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                                foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty(\"addresses\").EnumerateArray())\n                                {\n                                    IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                    if (address.AddressFamily == AddressFamily.InterNetwork)\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address)));\n                                }\n                                break;\n\n                            case DnsResourceRecordType.AAAA:\n                                foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty(\"addresses\").EnumerateArray())\n                                {\n                                    IPAddress address = IPAddress.Parse(jsonAddress.GetString());\n\n                                    if (address.AddressFamily == AddressFamily.InterNetworkV6)\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)));\n                                }\n                                break;\n                        }\n\n                        if (answers.Count == 0)\n                            return Task.FromResult<DnsDatagram>(null);\n\n                        if (answers.Count > 1)\n                            answers.Shuffle();\n\n                        EDnsOption[] options = null;\n\n                        if (requestECS is not null)\n                            options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address);\n\n                        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));\n                    }\n\n                default:\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"[\n  {\n    \"\"name\"\": \"\"server1-mumbai\"\",\n    \"\"lat\"\": \"\"19.07283\"\",\n    \"\"long\"\": \"\"72.88261\"\",\n    \"\"addresses\"\": [\n      \"\"1.1.1.1\"\"\n    ]\n  },\n  {\n    \"\"name\"\": \"\"server2-london\"\",\n    \"\"lat\"\": \"\"51.50853\"\",\n    \"\"long\"\": \"\"-0.12574\"\",\n    \"\"addresses\"\": [\n      \"\"2.2.2.2\"\"\n    ]\n  }\n]\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoDistanceApp/CNAME.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2.Model;\nusing MaxMind.GeoIP2.Responses;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace GeoDistance\n{\n    public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n        MaxMind _maxMind;\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_maxMind is not null)\n                    _maxMind.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region private\n\n        private static double GetDistance(double lat1, double long1, double lat2, double long2)\n        {\n            double d1 = lat1 * (Math.PI / 180.0);\n            double num1 = long1 * (Math.PI / 180.0);\n            double d2 = lat2 * (Math.PI / 180.0);\n            double num2 = long2 * (Math.PI / 180.0) - num1;\n            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);\n\n            return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _maxMind = MaxMind.Create(dnsServer);\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            Location location = null;\n\n            byte scopePrefixLength = 0;\n            EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n            if (requestECS is not null)\n            {\n                if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null))\n                    scopePrefixLength = (byte)csIsp.Network.PrefixLength;\n                else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null))\n                    scopePrefixLength = (byte)csAsn.Network.PrefixLength;\n                else\n                    scopePrefixLength = requestECS.SourcePrefixLength;\n\n                if (_maxMind.CityReader.TryCity(requestECS.Address, out CityResponse csResponse) && csResponse.Location.HasCoordinates)\n                    location = csResponse.Location;\n            }\n\n            if ((location is null) && _maxMind.CityReader.TryCity(remoteEP.Address, out CityResponse response) && response.Location.HasCoordinates)\n                location = response.Location;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n            JsonElement jsonAppRecordData = jsonDocument.RootElement;\n            JsonElement jsonClosestServer = default;\n\n            if (location is null)\n            {\n                if (jsonAppRecordData.GetArrayLength() > 0)\n                    jsonClosestServer = jsonAppRecordData[0];\n            }\n            else\n            {\n                double lastDistance = double.MaxValue;\n\n                foreach (JsonElement jsonServer in jsonAppRecordData.EnumerateArray())\n                {\n                    double lat = Convert.ToDouble(jsonServer.GetProperty(\"lat\").GetString());\n                    double @long = Convert.ToDouble(jsonServer.GetProperty(\"long\").GetString());\n\n                    double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value);\n\n                    if (distance < lastDistance)\n                    {\n                        lastDistance = distance;\n                        jsonClosestServer = jsonServer;\n                    }\n                }\n            }\n\n            if (jsonClosestServer.ValueKind == JsonValueKind.Undefined)\n                return Task.FromResult<DnsDatagram>(null);\n\n            string cname = jsonClosestServer.GetPropertyValue(\"cname\", null);\n            if (string.IsNullOrEmpty(cname))\n                return Task.FromResult<DnsDatagram>(null);\n\n            IReadOnlyList<DnsResourceRecord> answers;\n\n            if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME\n            else\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) };\n\n            EDnsOption[] options = null;\n\n            if (requestECS is not null)\n                options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address);\n\n            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));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"[\n  {\n    \"\"name\"\": \"\"server1-mumbai\"\",\n    \"\"lat\"\": \"\"19.07283\"\",\n    \"\"long\"\": \"\"72.88261\"\",\n    \"\"cname\"\": \"\"mumbai.example.com\"\"\n  },\n  {\n    \"\"name\"\": \"\"server2-london\"\",\n    \"\"lat\"\": \"\"51.50853\"\",\n    \"\"long\"\": \"\"-0.12574\"\",\n    \"\"cname\"\": \"\"london.example.com\"\"\n  }\n]\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoDistanceApp/GeoDistanceApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<Version>9.0.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>GeoDistanceApp</AssemblyName>\n\t\t<RootNamespace>GeoDistance</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"MaxMind.GeoIP2\" Version=\"5.4.1\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"ReadMe.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/GeoDistanceApp/MaxMind.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MaxMind.GeoIP2;\nusing System;\nusing System.IO;\n\nnamespace GeoDistance\n{\n    class MaxMind : IDisposable\n    {\n        #region variables\n\n        static MaxMind _maxMind;\n\n        readonly DatabaseReader _mmCityReader;\n        readonly DatabaseReader _mmIspReader;\n        readonly DatabaseReader _mmAsnReader;\n\n        #endregion\n\n        #region constructor\n\n        private MaxMind(IDnsServer dnsServer)\n        {\n            string mmCityFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoIP2-City.mmdb\");\n\n            if (!File.Exists(mmCityFile))\n                mmCityFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoLite2-City.mmdb\");\n\n            if (!File.Exists(mmCityFile))\n                throw new FileNotFoundException(\"MaxMind City file is missing!\");\n\n            _mmCityReader = new DatabaseReader(mmCityFile);\n\n            string mmIspFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoIP2-ISP.mmdb\");\n            if (File.Exists(mmIspFile))\n            {\n                _mmIspReader = new DatabaseReader(mmIspFile);\n                return;\n            }\n\n            string mmAsnFile = Path.Combine(dnsServer.ApplicationFolder, \"GeoLite2-ASN.mmdb\");\n            if (File.Exists(mmAsnFile))\n                _mmAsnReader = new DatabaseReader(mmAsnFile);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                _mmCityReader?.Dispose();\n                _mmIspReader?.Dispose();\n                _mmAsnReader?.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region public\n\n        public static MaxMind Create(IDnsServer dnsServer)\n        {\n            if (_maxMind is null)\n                _maxMind = new MaxMind(dnsServer);\n\n            return _maxMind;\n        }\n\n        #endregion\n\n        #region properties\n\n        public DatabaseReader CityReader\n        { get { return _mmCityReader; } }\n\n        public DatabaseReader IspReader\n        { get { return _mmIspReader; } }\n\n        public DatabaseReader AsnReader\n        { get { return _mmAsnReader; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/GeoDistanceApp/ReadMe.txt",
    "content": "﻿Using MaxMind GeoIP2 Database\n=============================\n\nWARNING: Latitude and longitude are not precise and should not be used to identify a particular street address or household.\n\nThis app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial.\n\nFor production usage, it is required that you purchase the GeoIP2 database from MaxMind (https://www.maxmind.com/) and use it. \n\nTo 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.\n\nThe app optionally also uses MaxMind ISP/ASN database which can be updated the with same method as above."
  },
  {
    "path": "Apps/GeoDistanceApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/LogExporterApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing LogExporter.Strategy;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace LogExporter\n{\n    public sealed class App : IDnsApplication, IDnsQueryLogger\n    {\n        #region variables\n\n        IDnsServer? _dnsServer;\n        AppConfig? _config;\n\n        readonly ExportManager _exportManager = new ExportManager();\n\n        bool _enableLogging;\n\n        readonly ConcurrentQueue<LogEntry> _queuedLogs = new ConcurrentQueue<LogEntry>();\n        readonly Timer _queueTimer;\n        const int QUEUE_TIMER_INTERVAL = 10000;\n        const int BULK_INSERT_COUNT = 1000;\n\n        bool _disposed;\n\n        #endregion\n\n        #region constructor\n\n        public App()\n        {\n            _queueTimer = new Timer(HandleExportLogCallback);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            Dispose(disposing: true);\n            GC.SuppressFinalize(this);\n        }\n\n        private void Dispose(bool disposing)\n        {\n            if (!_disposed)\n            {\n                if (disposing)\n                {\n                    _queueTimer?.Dispose();\n\n                    ExportLogsAsync().Sync(); //flush any pending logs\n\n                    _exportManager.Dispose();\n                }\n\n                _disposed = true;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _config = AppConfig.Deserialize(config);\n\n            if (_config is null)\n                throw new DnsClientException(\"Invalid application configuration.\");\n\n            if (_config.FileTarget!.Enabled)\n            {\n                _exportManager.RemoveStrategy(typeof(FileExportStrategy));\n                _exportManager.AddStrategy(new FileExportStrategy(_config.FileTarget!.Path));\n            }\n            else\n            {\n                _exportManager.RemoveStrategy(typeof(FileExportStrategy));\n            }\n\n            if (_config.HttpTarget!.Enabled)\n            {\n                _exportManager.RemoveStrategy(typeof(HttpExportStrategy));\n                _exportManager.AddStrategy(new HttpExportStrategy(_config.HttpTarget.Endpoint, _config.HttpTarget.Headers));\n            }\n            else\n            {\n                _exportManager.RemoveStrategy(typeof(HttpExportStrategy));\n            }\n\n            if (_config.SyslogTarget!.Enabled)\n            {\n                _exportManager.RemoveStrategy(typeof(SyslogExportStrategy));\n                _exportManager.AddStrategy(new SyslogExportStrategy(_config.SyslogTarget.Address, _config.SyslogTarget.Port, _config.SyslogTarget.Protocol));\n            }\n            else\n            {\n                _exportManager.RemoveStrategy(typeof(SyslogExportStrategy));\n            }\n\n            _enableLogging = _exportManager.HasStrategy();\n\n            if (_enableLogging)\n                _queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);\n            else\n                _queueTimer.Change(Timeout.Infinite, Timeout.Infinite);\n\n            return Task.CompletedTask;\n        }\n\n        public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (_enableLogging)\n            {\n                if (_queuedLogs.Count < _config!.MaxQueueSize)\n                    _queuedLogs.Enqueue(new LogEntry(timestamp, remoteEP, protocol, request, response, _config.EnableEdnsLogging));\n            }\n\n            return Task.CompletedTask;\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task ExportLogsAsync()\n        {\n            try\n            {\n                List<LogEntry> logs = new List<LogEntry>(BULK_INSERT_COUNT);\n\n                while (true)\n                {\n                    while (logs.Count < BULK_INSERT_COUNT && _queuedLogs.TryDequeue(out LogEntry? log))\n                    {\n                        logs.Add(log);\n                    }\n\n                    if (logs.Count < 1)\n                        break;\n\n                    await _exportManager.ImplementStrategyAsync(logs);\n\n                    logs.Clear();\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer?.WriteLog(ex);\n            }\n        }\n\n        private async void HandleExportLogCallback(object? state)\n        {\n            try\n            {\n                // Process logs within the timer interval, then let the timer reschedule\n                await ExportLogsAsync();\n            }\n            catch (Exception ex)\n            {\n                _dnsServer?.WriteLog(ex);\n            }\n            finally\n            {\n                try\n                {\n                    _queueTimer?.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);\n                }\n                catch (ObjectDisposedException)\n                { }\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        {\n            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).\"; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/LogExporterApp/AppConfig.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace LogExporter\n{\n    public class AppConfig\n    {\n        [JsonPropertyName(\"maxQueueSize\")]\n        public int MaxQueueSize { get; set; }\n\n        [JsonPropertyName(\"enableEdnsLogging \")]\n        public bool EnableEdnsLogging { get; set; }\n\n        [JsonPropertyName(\"file\")]\n        public FileTarget? FileTarget { get; set; }\n\n        [JsonPropertyName(\"http\")]\n        public HttpTarget? HttpTarget { get; set; }\n\n        [JsonPropertyName(\"syslog\")]\n        public SyslogTarget? SyslogTarget { get; set; }\n\n        // Load configuration from JSON\n        public static AppConfig? Deserialize(string json)\n        {\n            return JsonSerializer.Deserialize<AppConfig>(json, DnsConfigSerializerOptions.Default);\n        }\n    }\n\n    public class TargetBase\n    {\n        [JsonPropertyName(\"enabled\")]\n        public bool Enabled { get; set; }\n    }\n\n    public class SyslogTarget : TargetBase\n    {\n        [JsonPropertyName(\"address\")]\n        public required string Address { get; set; }\n\n        [JsonPropertyName(\"port\")]\n        public int? Port { get; set; }\n\n        [JsonPropertyName(\"protocol\")]\n        public string? Protocol { get; set; }\n    }\n\n    public class FileTarget : TargetBase\n    {\n        [JsonPropertyName(\"path\")]\n        public required string Path { get; set; }\n    }\n\n    public class HttpTarget : TargetBase\n    {\n        [JsonPropertyName(\"endpoint\")]\n        public required string Endpoint { get; set; }\n\n        [JsonPropertyName(\"headers\")]\n        public Dictionary<string, string?>? Headers { get; set; }\n    }\n\n    // Setup reusable options with a single instance\n    public static class DnsConfigSerializerOptions\n    {\n        public static readonly JsonSerializerOptions Default = new JsonSerializerOptions\n        {\n            PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Convert properties to camelCase\n            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // For safe encoding\n            NumberHandling = JsonNumberHandling.Strict,\n            AllowTrailingCommas = true, // Allow trailing commas in JSON\n            DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, // Convert dictionary keys to camelCase\n            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignore null values\n        };\n    }\n}\n"
  },
  {
    "path": "Apps/LogExporterApp/LogEntry.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace LogExporter\n{\n    public class LogEntry\n    {\n        public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response, bool ednsLogging = false)\n        {\n            // Assign timestamp and ensure it's in UTC\n            Timestamp = timestamp.Kind == DateTimeKind.Utc ? timestamp : timestamp.ToUniversalTime();\n\n            // Extract client information\n            ClientIp = remoteEP.Address.ToString();\n            Protocol = protocol;\n            ResponseType = response.Tag == null ? DnsServerResponseType.Recursive : (DnsServerResponseType)response.Tag;\n\n            if ((ResponseType == DnsServerResponseType.Recursive) && (response.Metadata is not null))\n                ResponseRtt = response.Metadata.RoundTripTime;\n\n            ResponseCode = response.RCODE;\n\n            // Extract request information\n            if (request.Question.Count > 0)\n            {\n                DnsQuestionRecord query = request.Question[0];\n\n                Question = new DnsQuestion\n                {\n                    QuestionName = query.Name,\n                    QuestionType = query.Type,\n                    QuestionClass = query.Class,\n                };\n            }\n\n            // Convert answer section into a simple string summary (comma-separated for multiple answers)\n            Answers = new List<DnsResourceRecord>(response.Answer.Count);\n            if (response.Answer.Count > 0)\n            {\n                Answers.AddRange(response.Answer.Select(record => new DnsResourceRecord\n                {\n                    Name = record.Name,\n                    RecordType = record.Type,\n                    RecordClass = record.Class,\n                    RecordTtl = record.TTL,\n                    RecordData = record.RDATA.ToString(),\n                    DnssecStatus = record.DnssecStatus,\n                }));\n            }\n\n            EDNS = new List<EDNSLog>();\n            if (!ednsLogging || response.EDNS is null)\n            {\n                return;\n            }\n\n            foreach (EDnsOption extendedErrorLog in response.EDNS.Options.Where(o => o.Code == EDnsOptionCode.EXTENDED_DNS_ERROR))\n            {\n                string[] extractedData = extendedErrorLog.Data.ToString().Replace(\"[\", string.Empty).Replace(\"]\", string.Empty).Split(\":\", StringSplitOptions.TrimEntries);\n\n                EDNS.Add(new EDNSLog\n                {\n                    ErrType = extractedData[0],\n                    Message = extractedData[1]\n                });\n            }\n        }\n\n        public List<DnsResourceRecord> Answers { get; private set; }\n        public string ClientIp { get; private set; }\n        public List<EDNSLog> EDNS { get; private set; }\n        public DnsTransportProtocol Protocol { get; private set; }\n        public DnsQuestion? Question { get; private set; }\n        public DnsResponseCode ResponseCode { get; private set; }\n        public double? ResponseRtt { get; private set; }\n        public DnsServerResponseType ResponseType { get; private set; }\n        public DateTime Timestamp { get; private set; }\n        public override string ToString()\n        {\n            return JsonSerializer.Serialize(this, DnsLogSerializerOptions.Default);\n        }\n\n        public static class DnsLogSerializerOptions\n        {\n            public static readonly JsonSerializerOptions Default = new JsonSerializerOptions\n            {\n                WriteIndented = false,\n                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n                Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() },\n                Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,\n                NumberHandling = JsonNumberHandling.Strict,\n                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull\n            };\n        }\n\n        public class DnsQuestion\n        {\n            public DnsClass QuestionClass { get; set; }\n            public required string QuestionName { get; set; }\n            public DnsResourceRecordType QuestionType { get; set; }\n        }\n\n        public class DnsResourceRecord\n        {\n            public DnssecStatus DnssecStatus { get; set; }\n            public required string Name { get; set; }\n            public DnsClass RecordClass { get; set; }\n            public required string RecordData { get; set; }\n            public uint RecordTtl { get; set; }\n            public DnsResourceRecordType RecordType { get; set; }\n        }\n\n        public class EDNSLog\n        {\n            public string? ErrType { get; set; }\n            public string? Message { get; set; }\n        }\n\n        public class JsonDateTimeConverter : JsonConverter<DateTime>\n        {\n            public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n            {\n                string? dts = reader.GetString();\n                return dts == null ? DateTime.MinValue : DateTime.Parse(dts);\n            }\n\n            public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)\n            {\n                writer.WriteStringValue(value.ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:ss.fffZ\"));\n            }\n        }\n    }\n}"
  },
  {
    "path": "Apps/LogExporterApp/LogExporterApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    <Version>2.1</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Zafer Balkan</Authors>\n    <AssemblyName>LogExporterApp</AssemblyName>\n    <RootNamespace>LogExporter</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <Description>Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols).</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Configuration\" Version=\"9.0.10\" />\n    <PackageReference Include=\"Serilog.Sinks.File\" Version=\"7.0.0\" />\n    <PackageReference Include=\"Serilog.Sinks.Http\" Version=\"9.2.0\" />\n    <PackageReference Include=\"Serilog.Sinks.PeriodicBatching\" Version=\"5.0.0\" />\n    <PackageReference Include=\"Serilog.Sinks.SyslogMessages\" Version=\"4.0.0\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/LogExporterApp/Strategy/ExportManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace LogExporter.Strategy\n{\n    public sealed class ExportManager : IDisposable\n    {\n        #region variables\n\n        readonly ConcurrentDictionary<Type, IExportStrategy> _exportStrategies = new ConcurrentDictionary<Type, IExportStrategy>();\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            foreach (KeyValuePair<Type, IExportStrategy> exportStrategy in _exportStrategies)\n                exportStrategy.Value.Dispose();\n        }\n\n        #endregion\n\n        #region public\n\n        public void AddStrategy(IExportStrategy strategy)\n        {\n            if (!_exportStrategies.TryAdd(strategy.GetType(), strategy))\n                throw new InvalidOperationException();\n        }\n\n        public void RemoveStrategy(Type type)\n        {\n            if (_exportStrategies.TryRemove(type, out IExportStrategy? existing))\n                existing?.Dispose();\n        }\n\n        public bool HasStrategy()\n        {\n            return !_exportStrategies.IsEmpty;\n        }\n\n        public async Task ImplementStrategyAsync(IReadOnlyList<LogEntry> logs)\n        {\n            List<Task> tasks = new List<Task>(_exportStrategies.Count);\n\n            foreach (KeyValuePair<Type, IExportStrategy> strategy in _exportStrategies)\n            {\n                tasks.Add(Task.Factory.StartNew(delegate (object? state)\n                {\n                    return strategy.Value.ExportAsync(logs);\n                }, null, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current));\n            }\n\n            await Task.WhenAll(tasks);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/LogExporterApp/Strategy/FileExportStrategy.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing Serilog;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace LogExporter.Strategy\n{\n    public sealed class FileExportStrategy : IExportStrategy\n    {\n        #region variables\n\n        readonly Serilog.Core.Logger _sender;\n\n        bool _disposed;\n\n        #endregion\n\n        #region constructor\n\n        public FileExportStrategy(string filePath)\n        {\n            _sender = new LoggerConfiguration().WriteTo.File(filePath, outputTemplate: \"{Message:lj}{NewLine}{Exception}\").CreateLogger();\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            if (!_disposed)\n            {\n                _sender.Dispose();\n\n                _disposed = true;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public Task ExportAsync(IReadOnlyList<LogEntry> logs)\n        {\n            foreach (LogEntry logEntry in logs)\n                _sender.Information(logEntry.ToString());\n\n            return Task.CompletedTask;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/LogExporterApp/Strategy/HttpExportStrategy.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing Microsoft.Extensions.Configuration;\nusing Serilog;\nusing Serilog.Sinks.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace LogExporter.Strategy\n{\n    public sealed class HttpExportStrategy : IExportStrategy\n    {\n        #region variables\n\n        readonly Serilog.Core.Logger _sender;\n\n        bool _disposed;\n\n        #endregion\n\n        #region constructor\n\n        public HttpExportStrategy(string endpoint, Dictionary<string, string?>? headers = null)\n        {\n            IConfigurationRoot? configuration = null;\n            if (headers != null)\n            {\n                configuration = new ConfigurationBuilder()\n               .AddInMemoryCollection(headers)\n               .Build();\n            }\n\n            _sender = new LoggerConfiguration().WriteTo.Http(endpoint, null, httpClient: new CustomHttpClient(), configuration: configuration).Enrich.FromLogContext().CreateLogger();\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            if (!_disposed)\n            {\n                _sender.Dispose();\n\n                _disposed = true;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public Task ExportAsync(IReadOnlyList<LogEntry> logs)\n        {\n            foreach (LogEntry logEntry in logs)\n                _sender.Information(logEntry.ToString());\n\n            return Task.CompletedTask;\n        }\n\n        #endregion\n\n        public class CustomHttpClient : IHttpClient\n        {\n            readonly HttpClient _httpClient;\n\n            public CustomHttpClient()\n            {\n                _httpClient = new HttpClient();\n            }\n\n            public void Configure(IConfiguration configuration)\n            {\n                foreach (IConfigurationSection pair in configuration.GetChildren())\n                {\n                    if (!_httpClient.DefaultRequestHeaders.TryAddWithoutValidation(pair.Key, pair.Value))\n                        throw new FormatException($\"Failed to add header '{pair.Key}'.\");\n                }\n            }\n\n            public void Dispose()\n            {\n                _httpClient?.Dispose();\n                GC.SuppressFinalize(this);\n            }\n\n            public async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken)\n            {\n                StreamContent content = new StreamContent(contentStream);\n                content.Headers.Add(\"Content-Type\", \"application/json\");\n\n                return await _httpClient\n                    .PostAsync(requestUri, content, cancellationToken)\n                    .ConfigureAwait(false);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/LogExporterApp/Strategy/IExportStrategy.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace LogExporter.Strategy\n{\n    /// <summary>\n    ///     Strategy interface to decide the sinks for exporting the logs.\n    /// </summary>\n    public interface IExportStrategy: IDisposable\n    {\n        Task ExportAsync(IReadOnlyList<LogEntry> logs);\n    }\n}\n"
  },
  {
    "path": "Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing Serilog;\nusing Serilog.Events;\nusing Serilog.Parsing;\nusing Serilog.Sinks.Syslog;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\n\nnamespace LogExporter.Strategy\n{\n    public sealed class SyslogExportStrategy : IExportStrategy\n    {\n        #region variables\n\n        const string _appName = \"Technitium DNS Server\";\n        const string _sdId = \"meta\";\n        const string DEFAUL_PROTOCOL = \"udp\";\n        const int DEFAULT_PORT = 514;\n\n        readonly Facility _facility = Facility.Local6;\n\n        readonly Rfc5424Formatter _formatter;\n        readonly Serilog.Core.Logger _sender;\n\n        bool _disposed;\n\n        #endregion\n\n        #region constructor\n\n        public SyslogExportStrategy(string address, int? port, string? protocol)\n        {\n            port ??= DEFAULT_PORT;\n            protocol ??= DEFAUL_PROTOCOL;\n\n            LoggerConfiguration conf = new LoggerConfiguration();\n\n            _sender = protocol.ToLowerInvariant() switch\n            {\n                \"tls\" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: true).Enrich.FromLogContext().CreateLogger(),\n                \"tcp\" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: false).Enrich.FromLogContext().CreateLogger(),\n                \"udp\" => conf.WriteTo.UdpSyslog(address, port.Value, _appName, SyslogFormat.RFC5424, _facility).Enrich.FromLogContext().CreateLogger(),\n                \"local\" => conf.WriteTo.LocalSyslog(_appName, _facility).Enrich.FromLogContext().CreateLogger(),\n                _ => throw new NotSupportedException(\"Syslog protocol is not supported: \" + protocol),\n            };\n\n            _formatter = new Rfc5424Formatter(_facility, _appName, null, _sdId, Environment.MachineName);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            if (!_disposed)\n            {\n                _sender.Dispose();\n\n                _disposed = true;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public Task ExportAsync(IReadOnlyList<LogEntry> logs)\n        {\n            foreach (LogEntry log in logs)\n                _sender.Information(_formatter.FormatMessage((LogEvent?)Convert(log)));\n\n            return Task.CompletedTask;\n        }\n\n        #endregion\n\n        #region private\n\n        private static LogEvent Convert(LogEntry log)\n        {\n            // Initialize properties with base log details\n            List<LogEventProperty> properties = new List<LogEventProperty>\n            {\n                new LogEventProperty(\"timestamp\", new ScalarValue(log.Timestamp.ToString(\"yyyy-MM-ddTHH:mm:ss.fffZ\"))),\n                new LogEventProperty(\"clientIp\", new ScalarValue(log.ClientIp)),\n                new LogEventProperty(\"protocol\", new ScalarValue(log.Protocol.ToString())),\n                new LogEventProperty(\"responseType\", new ScalarValue(log.ResponseType.ToString())),\n                new LogEventProperty(\"responseRtt\", new ScalarValue(log.ResponseRtt?.ToString())),\n                new LogEventProperty(\"rCode\", new ScalarValue(log.ResponseCode.ToString()))\n            };\n\n            // Add each question as properties\n            if (log.Question != null)\n            {\n                LogEntry.DnsQuestion question = log.Question;\n                properties.Add(new LogEventProperty(\"qName\", new ScalarValue(question.QuestionName)));\n                properties.Add(new LogEventProperty(\"qType\", new ScalarValue(question.QuestionType.ToString())));\n                properties.Add(new LogEventProperty(\"qClass\", new ScalarValue(question.QuestionClass.ToString())));\n\n                string questionSummary = $\"QNAME: {question.QuestionName}, QTYPE: {question.QuestionType}, QCLASS: {question.QuestionClass}\";\n                properties.Add(new LogEventProperty(\"questionsSummary\", new ScalarValue(questionSummary)));\n            }\n            else\n            {\n                properties.Add(new LogEventProperty(\"questionsSummary\", new ScalarValue(string.Empty)));\n            }\n\n            // Add each answer as properties\n            if (log.Answers.Count > 0)\n            {\n                for (int i = 0; i < log.Answers.Count; i++)\n                {\n                    LogEntry.DnsResourceRecord answer = log.Answers[i];\n\n                    properties.Add(new LogEventProperty($\"aName_{i}\", new ScalarValue(answer.Name)));\n                    properties.Add(new LogEventProperty($\"aType_{i}\", new ScalarValue(answer.RecordType.ToString())));\n                    properties.Add(new LogEventProperty($\"aClass_{i}\", new ScalarValue(answer.RecordClass.ToString())));\n                    properties.Add(new LogEventProperty($\"aTtl_{i}\", new ScalarValue(answer.RecordTtl.ToString())));\n                    properties.Add(new LogEventProperty($\"aRData_{i}\", new ScalarValue(answer.RecordData)));\n                    properties.Add(new LogEventProperty($\"aDnssecStatus_{i}\", new ScalarValue(answer.DnssecStatus.ToString())));\n                }\n\n                // Generate answers summary\n                string answerSummary = string.Join(\", \", log.Answers.Select(a => a.RecordData));\n                properties.Add(new LogEventProperty(\"answersSummary\", new ScalarValue(answerSummary)));\n            }\n            else\n            {\n                properties.Add(new LogEventProperty(\"answersSummary\", new ScalarValue(string.Empty)));\n            }\n\n            // Add EDNS logs\n            if (log.EDNS.Count > 0)\n            {\n                for (int i = 0; i < log.EDNS.Count; i++)\n                {\n                    var ednsLog = log.EDNS[i];\n                    properties.Add(new LogEventProperty($\"ednsErrType_{i}\", new ScalarValue(ednsLog.ErrType)));\n                    properties.Add(new LogEventProperty($\"ednsMessage_{i}\", new ScalarValue(ednsLog.Message)));\n\n                }\n            }\n\n            // Define the message template to match the original summary format\n            const string templateText = \"{questionsSummary}; RCODE: {rCode}; ANSWER: [{answersSummary}]\";\n\n            // Parse the template\n            MessageTemplate template = new MessageTemplateParser().Parse(templateText);\n\n            // Create the LogEvent and return it\n            return new LogEvent(\n                timestamp: log.Timestamp,\n                level: LogEventLevel.Information,\n                exception: null,\n                messageTemplate: template,\n                properties: properties\n            );\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/LogExporterApp/dnsApp.config",
    "content": "{\n\t\"maxQueueSize\": 1000000,\n\t\"ebableEdnsLogging\": false,\n\t\"file\": {\n\t\t\"path\": \"./dns_logs.json\",\n\t\t\"enabled\": false\n\t},\n\t\"http\": {\n\t\t\"endpoint\": \"http://localhost:5000/logs\",\n\t\t\"headers\": {\n\t\t\"Authorization\": \"Bearer abc123\"\n\t\t},\n\t\t\"enabled\": false\n\t},\n\t\"syslog\": {\n\t\t\"address\": \"127.0.0.1\",\n\t\t\"port\": 514,\n\t\t\"protocol\": \"UDP\",\n\t\t\"enabled\": false\n\t}\n}"
  },
  {
    "path": "Apps/MispConnectorApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nCopyright (C) 2025  Zafer Balkan (zafer@zaferbalkan.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.ComponentModel.DataAnnotations;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Security;\nusing System.Net.Sockets;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Http.Client;\n\nnamespace MispConnector\n{\n    public sealed class App : IDnsApplication, IDnsRequestBlockingHandler\n    {\n        #region variables\n\n        string _domainCacheFilePath;\n        Config _config;\n        IDnsServer _dnsServer;\n        FrozenSet<string> _domainBlocklist = FrozenSet<string>.Empty;\n        HttpClient _httpClient;\n\n        Uri _mispApiUrl;\n\n        DnsSOARecordData _soaRecord;\n        TimeSpan _updateInterval;\n        Task _updateLoopTask;\n\n        CancellationTokenSource _appShutdownCts;\n        #endregion variables\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            _appShutdownCts?.Cancel();\n            try\n            {\n                if (_updateLoopTask != null)\n                {\n                    _ = Task.WhenAny(_updateLoopTask, Task.Delay(TimeSpan.FromSeconds(2))).GetAwaiter().GetResult();\n                }\n            }\n            catch\n            {\n            }\n            finally\n            {\n                _appShutdownCts?.Dispose();\n                _httpClient?.Dispose();\n            }\n        }\n\n        #endregion IDisposable\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            try\n            {\n                _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, 60);\n\n                JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };\n                _config = JsonSerializer.Deserialize<Config>(config, options);\n\n                Validator.ValidateObject(_config, new ValidationContext(_config), validateAllProperties: true);\n\n                string configDir = _dnsServer.ApplicationFolder;\n                Directory.CreateDirectory(configDir);\n                _domainCacheFilePath = Path.Combine(configDir, \"misp_domain_cache.txt\");\n\n                _updateInterval = ParseUpdateInterval(_config.UpdateInterval);\n\n                Uri mispServerUrl = new Uri(_config.MispServerUrl);\n                _mispApiUrl = new Uri(mispServerUrl, \"/attributes/restSearch\");\n                _httpClient = CreateHttpClient(mispServerUrl, _config.DisableTlsValidation);\n\n                await LoadBlocklistFromCacheAsync();\n                _appShutdownCts = new CancellationTokenSource();\n\n                // We do not await this, as it's designed to run for the lifetime of the app.\n                _updateLoopTask = StartUpdateLoopAsync(_appShutdownCts.Token);\n                Task _ = _updateLoopTask.ContinueWith(t =>\n                {\n                    if (t.IsFaulted)\n                    {\n                        _dnsServer.WriteLog($\"FATAL: Update loop terminated unexpectedly: {t.Exception?.GetBaseException().Message}\");\n                        _dnsServer.WriteLog(t.Exception);\n                    }\n                }, TaskContinuationOptions.OnlyOnFaulted);\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.WriteLog($\"FATAL: MISP Connector failed to initialize. Check configuration. Error: {ex.Message}\");\n                _dnsServer.WriteLog(ex);\n            }\n        }\n\n        public Task<bool> IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP)\n        {\n            return Task.FromResult(false);\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP)\n        {\n            if (_config?.EnableBlocking != true)\n            {\n                return Task.FromResult<DnsDatagram>(null);\n            }\n\n            DnsQuestionRecord question = request.Question[0];\n            bool domainBlocked = IsDomainBlocked(question.Name, out string blockedDomain);\n            if (!domainBlocked)\n            {\n                return Task.FromResult<DnsDatagram>(null);\n            }\n\n            string blockingReport = $\"source=misp-connector;domain={blockedDomain}\";\n\n            EDnsOption[] options = null;\n            if (_config.AddExtendedDnsError && request.EDNS is not null)\n            {\n                options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, string.Empty)) };\n            }\n\n            if (_config.AllowTxtBlockingReport && question.Type == DnsResourceRecordType.TXT)\n            {\n                DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData(string.Empty)) };\n                return Task.FromResult(new DnsDatagram(\n                                    ID: request.Identifier,\n                                    isResponse: true,\n                                    OPCODE: DnsOpcode.StandardQuery,\n                                    authoritativeAnswer: false,\n                                    truncation: false,\n                                    recursionDesired: request.RecursionDesired,\n                                    recursionAvailable: true,\n                                    authenticData: false,\n                                    checkingDisabled: false,\n                                    RCODE: DnsResponseCode.NoError,\n                                    question: request.Question,\n                                    answer: answer,\n                                    authority: null,\n                                    additional: null,\n                                    udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize,\n                                    ednsFlags: EDnsHeaderFlags.None,\n                                    options: options\n                                ));\n            }\n\n            DnsResourceRecord[] authority = { new DnsResourceRecord(question.Name, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) };\n            return Task.FromResult(new DnsDatagram(\n                            ID: request.Identifier,\n                            isResponse: true,\n                            OPCODE: DnsOpcode.StandardQuery,\n                            authoritativeAnswer: true,\n                            truncation: false,\n                            recursionDesired: request.RecursionDesired,\n                            recursionAvailable: true,\n                            authenticData: false,\n                            checkingDisabled: false,\n                            RCODE: DnsResponseCode.NxDomain,\n                            question: request.Question,\n                            answer: null,\n                            authority: authority,\n                            additional: null,\n                            udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize,\n                            ednsFlags: EDnsHeaderFlags.None,\n                            options: options\n                        ));\n        }\n\n        #endregion public\n\n        #region private\n        private async Task StartUpdateLoopAsync(CancellationToken cancellationToken)\n        {\n            await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(5, 30)), cancellationToken);\n            using (PeriodicTimer timer = new PeriodicTimer(_updateInterval))\n            {\n                while (!cancellationToken.IsCancellationRequested)\n                {\n                    try\n                    {\n                        await UpdateIocsAsync(cancellationToken);\n                    }\n                    catch (OperationCanceledException)\n                    {\n                        _dnsServer.WriteLog(\"Update loop is shutting down gracefully.\");\n                        break;\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.WriteLog($\"FATAL: The MispConnector update task failed unexpectedly. Error: {ex.Message}\");\n                        _dnsServer.WriteLog(ex);\n                    }\n\n                    await timer.WaitForNextTickAsync(cancellationToken);\n                }\n            }\n        }\n        private static TimeSpan ParseUpdateInterval(string interval)\n        {\n            if (string.IsNullOrWhiteSpace(interval) || interval.Length < 2)\n            {\n                throw new FormatException(\"Update interval is not in a valid format (e.g., '60m', '2h', '7d').\");\n            }\n\n            string unit = interval.Substring(interval.Length - 1).ToLowerInvariant();\n            string valueString = interval.Substring(0, interval.Length - 1);\n\n            if (!int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) || value <= 0)\n            {\n                throw new FormatException($\"Invalid numeric value '{valueString}' in update interval.\");\n            }\n\n            switch (unit)\n            {\n                case \"m\":\n                    return TimeSpan.FromMinutes(value);\n\n                case \"h\":\n                    return TimeSpan.FromHours(value);\n\n                case \"d\":\n                    return TimeSpan.FromDays(value);\n\n                default:\n                    throw new FormatException($\"Invalid unit '{unit}' in update interval. Allowed units are 'm', 'h', 'd'.\");\n            }\n        }\n\n        private async Task<bool> CheckTcpPortAsync(Uri serverUri, CancellationToken cancellationToken)\n        {\n            string host = serverUri.DnsSafeHost;\n            int port = serverUri.Port;\n            TimeSpan timeout = TimeSpan.FromSeconds(5);\n\n            _dnsServer.WriteLog($\"Performing pre-flight TCP check for {host}:{port} with a {timeout.TotalSeconds}-second timeout...\");\n\n            try\n            {\n                using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(timeout).Token))\n                using (TcpClient client = new TcpClient())\n                {\n                    await client.ConnectAsync(host, port, cts.Token);\n                }\n\n                _dnsServer.WriteLog($\"Pre-flight TCP check successful for {host}:{port}.\");\n                return true;\n            }\n            catch (OperationCanceledException)\n            {\n                _dnsServer.WriteLog($\"ERROR: Pre-flight TCP check failed: Connection to {host}:{port} timed out after {timeout.TotalSeconds} seconds. Check firewall rules or network route.\");\n                return false;\n            }\n            catch (SocketException ex)\n            {\n                _dnsServer.WriteLog($\"ERROR: Pre-flight TCP check failed: A network error occurred for {host}:{port}. Error: {ex.Message}\");\n                return false;\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.WriteLog($\"ERROR: An unexpected error occurred during the pre-flight TCP check for {host}:{port}. Error: {ex.Message}\");\n                return false;\n            }\n        }\n\n        private HttpClient CreateHttpClient(Uri serverUrl, bool disableTlsValidation)\n        {\n            HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n            handler.Proxy = _dnsServer.Proxy;\n            handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n            handler.DnsClient = _dnsServer;\n\n            if (disableTlsValidation)\n            {\n                handler.InnerHandler.SslOptions.RemoteCertificateValidationCallback = delegate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)\n                {\n                    return true;\n                };\n\n                _dnsServer.WriteLog($\"WARNING: TLS certificate validation is DISABLED for MISP server: {serverUrl}\");\n            }\n\n            return new HttpClient(handler);\n        }\n\n        private async Task<HashSet<string>> FetchIocFromMispAsync(CancellationToken cancellationToken)\n        {\n            HashSet<string> iocSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            int page = 1;\n            int limit = _config.PaginationLimit;\n            bool hasMorePages = true;\n\n            _dnsServer.WriteLog($\"Starting paginated fetch from MISP API with a page size of {limit}...\");\n            const int maxRetries = 3;\n\n            while (hasMorePages)\n            {\n                int attempt = 0;\n                MispResponse mispResponse = null;\n\n                while (attempt < maxRetries)\n                {\n                    attempt++;\n                    try\n                    {\n                        MispRequestBody requestBody = new MispRequestBody\n                        {\n                            Type = \"domain\",\n                            To_ids = true,\n                            Deleted = false,\n                            Last = _config.MaxIocAge,\n                            Limit = limit,\n                            Page = page\n                        };\n                        StringContent requestContent = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, \"application/json\");\n\n                        using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _mispApiUrl) { Content = requestContent };\n                        request.Headers.Add(\"Authorization\", _config.MispApiKey);\n                        request.Headers.Add(\"Accept\", \"application/json\");\n\n                        _dnsServer.WriteLog($\"Fetching page {page}, attempt {attempt}/{maxRetries}...\");\n                        using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);\n\n                        if (!response.IsSuccessStatusCode)\n                        {\n                            // This is a definitive failure from the server (e.g., 403, 500).\n                            // We should not retry this. Abort immediately.\n                            string errorBody = await response.Content.ReadAsStringAsync(cancellationToken);\n                            throw new HttpRequestException($\"MISP API returned a non-success status code: {(int)response.StatusCode}. Body: {errorBody}\", null, response.StatusCode);\n                        }\n\n                        await using (Stream responseStream = await response.Content.ReadAsStreamAsync(cancellationToken))\n                            mispResponse = await JsonSerializer.DeserializeAsync<MispResponse>(responseStream, cancellationToken: cancellationToken);\n\n                        break;\n                    }\n                    catch (Exception ex) when (ex is HttpRequestException || ex is SocketException || ex is OperationCanceledException)\n                    {\n                        // These are likely transient network errors, so we should retry.\n                        _dnsServer.WriteLog($\"WARNING: A transient network error occurred on page {page}, attempt {attempt}/{maxRetries}. Error: {ex.Message}\");\n                        if (attempt < maxRetries)\n                        {\n                            TimeSpan delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000));\n                            _dnsServer.WriteLog($\"Waiting for {delay.TotalSeconds:F1} seconds before retrying...\");\n                            await Task.Delay(delay, cancellationToken);\n                        }\n                        else\n                        {\n                            // All retries have failed for this page.\n                            _dnsServer.WriteLog($\"ERROR: Failed to fetch page {page} after {maxRetries} attempts. Aborting entire update cycle.\");\n                            throw;\n                        }\n                    }\n                }\n\n                List<MispAttribute> attributes = mispResponse?.Response?.Attribute;\n                if (attributes == null || attributes.Count == 0)\n                {\n                    hasMorePages = false;\n                    continue;\n                }\n\n                foreach (MispAttribute attribute in attributes)\n                {\n                    string ioc = attribute.Value?.Trim().ToLowerInvariant();\n                    if (!string.IsNullOrEmpty(ioc))\n                    {\n                        if (DnsClient.IsDomainNameValid(ioc))\n                        {\n                            iocSet.Add(ioc);\n                        }\n                    }\n                }\n\n                // Assumption: If we received fewer items than our limit, it must be the last page.\n                if (attributes.Count < limit)\n                {\n                    hasMorePages = false;\n                }\n                else\n                {\n                    page++;\n                }\n            }\n\n            _dnsServer.WriteLog($\"Finished paginated fetch. Freezing {iocSet.Count} IOCs for optimal read performance...\");\n            return iocSet;\n        }\n\n        private bool IsDomainBlocked(string domain, out string foundZone)\n        {\n            FrozenSet<string> currentBlocklist = _domainBlocklist;\n\n            ReadOnlySpan<char> currentSpan = domain.AsSpan();\n\n            while (true)\n            {\n                // To look up in a HashSet<string>, we must provide a string.\n                string key = new string(currentSpan);\n                if (currentBlocklist.TryGetValue(key, out foundZone))\n                {\n                    return true;\n                }\n\n                int dotIndex = currentSpan.IndexOf('.');\n                if (dotIndex == -1)\n                {\n                    break; // No more parent domains.\n                }\n\n                // Slice to the parent domain view. No allocation here.\n                currentSpan = currentSpan.Slice(dotIndex + 1);\n            }\n\n            foundZone = null;\n            return false;\n        }\n\n        private async Task LoadBlocklistFromCacheAsync()\n        {\n            if (File.Exists(_domainCacheFilePath))\n            {\n                try\n                {\n                    FrozenSet<string> domains = (await File.ReadAllLinesAsync(_domainCacheFilePath)).ToHashSet(StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n                    Interlocked.Exchange(ref _domainBlocklist, domains);\n                    _dnsServer.WriteLog($\"MISP Connector: Loaded {domains.Count} domains from cache.\");\n                }\n                catch (IOException ex)\n                {\n                    _dnsServer.WriteLog($\"ERROR: Failed to read cache file '{_domainCacheFilePath}'. Error: {ex.Message}\");\n                }\n            }\n        }\n\n        private async Task UpdateIocsAsync(CancellationToken cancellationToken)\n        {\n            if (!await CheckTcpPortAsync(new Uri(_config.MispServerUrl), cancellationToken))\n            {\n                return;\n            }\n\n            _dnsServer.WriteLog(\"MISP Connector: Starting IOC update...\");\n\n            HashSet<string> tmpDomains = await FetchIocFromMispAsync(cancellationToken);\n            cancellationToken.ThrowIfCancellationRequested();\n            FrozenSet<string> domains = tmpDomains.ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n\n            if (!domains.SetEquals(_domainBlocklist))\n            {\n                await WriteIocsToCacheAsync(domains, cancellationToken);\n                Interlocked.Exchange(ref _domainBlocklist, domains);\n                _dnsServer.WriteLog($\"MISP Connector: Successfully updated blocklist with {domains.Count} domains.\");\n            }\n            else\n            {\n                _dnsServer.WriteLog(\"MISP data has not changed. No update to blocklist or cache is necessary.\");\n            }\n        }\n\n        private async Task WriteIocsToCacheAsync(FrozenSet<string> iocs, CancellationToken cancellationToken)\n        {\n            string tempPath = _domainCacheFilePath + \".tmp\";\n            await File.WriteAllLinesAsync(tempPath, iocs, cancellationToken);\n            File.Move(tempPath, _domainCacheFilePath, true);\n        }\n\n        #endregion private\n\n        #region properties\n\n        public string Description\n        {\n            get\n            {\n                return \"A focused connector that imports domain IOCs from a MISP server to block malicious domains using direct REST API calls.\";\n            }\n        }\n\n        #endregion properties\n\n        private class Config\n        {\n            [JsonPropertyName(\"addExtendedDnsError\")]\n            public bool AddExtendedDnsError { get; set; } = true;\n\n            [JsonPropertyName(\"allowTxtBlockingReport\")]\n            public bool AllowTxtBlockingReport { get; set; } = true;\n\n            [JsonPropertyName(\"disableTlsValidation\")]\n            public bool DisableTlsValidation { get; set; } = false;\n\n            [JsonPropertyName(\"enableBlocking\")]\n            public bool EnableBlocking { get; set; } = true;\n            [JsonPropertyName(\"maxIocAge\")]\n            [Required(ErrorMessage = \"maxIocAge is a required configuration property.\")]\n            [RegularExpression(@\"^\\d+[mhd]$\", ErrorMessage = \"Invalid interval format. Use a number followed by 'm', 'h', or 'd' (e.g., '90m', '2h', '7d').\", MatchTimeoutInMilliseconds = 3000)]\n            public string MaxIocAge { get; set; }\n\n            [JsonPropertyName(\"mispApiKey\")]\n            [Required(ErrorMessage = \"mispApiKey is a required configuration property.\")]\n            [MinLength(1, ErrorMessage = \"mispApiKey cannot be empty.\")]\n            public string MispApiKey { get; set; }\n\n            [JsonPropertyName(\"mispServerUrl\")]\n            [Required(ErrorMessage = \"mispServerUrl is a required configuration property.\")]\n            [Url(ErrorMessage = \"mispServerUrl must be a valid URL.\")]\n            public string MispServerUrl { get; set; }\n            [JsonPropertyName(\"paginationLimit\")]\n            public int PaginationLimit { get; set; } = 5000;\n\n            [JsonPropertyName(\"updateInterval\")]\n            [Required(ErrorMessage = \"updateInterval is a required configuration property.\")]\n            [RegularExpression(@\"^\\d+[mhd]$\", ErrorMessage = \"Invalid interval format. Use a number followed by 'm', 'h', or 'd' (e.g., '90m', '2h', '7d').\", MatchTimeoutInMilliseconds = 3000)]\n            public string UpdateInterval { get; set; }\n        }\n\n        private class MispAttribute\n        {\n            [JsonPropertyName(\"value\")]\n            public string Value { get; set; }\n        }\n\n        private class MispRequestBody\n        {\n            [JsonPropertyName(\"deleted\")]\n            public bool Deleted { get; set; }\n\n            [JsonPropertyName(\"last\")]\n            public string Last { get; set; }\n\n            [JsonPropertyName(\"limit\")]\n            public int Limit { get; set; }\n\n            [JsonPropertyName(\"page\")]\n            public int Page { get; set; }\n\n            [JsonPropertyName(\"to_ids\")]\n            public bool To_ids { get; set; }\n\n            [JsonPropertyName(\"type\")]\n            public string Type { get; set; }\n        }\n\n        private class MispResponse\n        {\n            [JsonPropertyName(\"response\")]\n            public MispResponseData Response { get; set; }\n        }\n\n        private class MispResponseData\n        {\n            [JsonPropertyName(\"Attribute\")]\n            public List<MispAttribute> Attribute { get; set; }\n        }\n    }\n}"
  },
  {
    "path": "Apps/MispConnectorApp/MispConnectorApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>1.0</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Zafer Balkan</Authors>\n\t\t<AssemblyName>MispConnectorApp</AssemblyName>\n\t\t<RootNamespace>MispConnector</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/MispConnectorApp/README.md",
    "content": "# MISP Connector for Technitium DNS Server\n\nA plugin that pulls malicious domain names from MISP feeds and enforces blocking in Technitium DNS.\n\nIt maintains in-memory blocklists with disk-backed caching and periodically refreshes from the source.\n\n## Features\n\n- Retrieves indicators of compromise (IOCs) aka. malicious domain names from a MISP server via its REST API.\n- Handles paginated fetches with exponential backoff and retry on transient failures.\n- Stores the latest blocklist in memory for fast lookup and persists it to disk for faster startup.\n- Blocks matching DNS requests by returning NXDOMAIN or, for TXT queries when enabled, a human-readable blocking report.\n- Optionally includes extended DNS error metadata.\n- Configurable refresh interval and age window for which indicators are considered.\n- Optional disabling of TLS certificate validation with explicit warning in logs.\n\n## Configuration\n\nSupply a JSON configuration like the following:\n\n```json\n{\n\t\"enableBlocking\": true,\n\t\"mispServerUrl\": \"https://misp.example.com\",\n\t\"mispApiKey\": \"YourMispApiKeyHere\",\n\t\"disableTlsValidation\": false,\n\t\"updateInterval\": \"2h\",\n\t\"maxIocAge\": \"15d\",\n\t\"allowTxtBlockingReport\": true,\n\t\"paginationLimit\": 5000,\n\t\"addExtendedDnsError\": true\n}\n```\n\n- You can disable the app without uninstalling.\n- You can disable TLS validation for test instances and homelabs, but **it is not recommended use this option in production**.\n- The `maxIocAge` option is used for filtering IOCs wih `lastSeen` attributes on MISP. So, you can dynamically filter for recent campaigns.\n- The `allowTxtBlockingReport` rewrites the response with a blocking report.\n- The `addExtendedDnsError` is useful when logs are exported to a SIEM. The blocking report gets added to EDNS payload of the package.\n\n# Acknowledgement\n\nThanks to everyone who has been part of or contributed to [MISP Project](https://www.misp-project.org/) for being an amazing resource."
  },
  {
    "path": "Apps/MispConnectorApp/dnsApp.config",
    "content": "{\n\t\"enableBlocking\": true,\n\t\"mispServerUrl\": \"https://misp.example.com\",\n\t\"mispApiKey\": \"YourMispApiKeyHere\",\n\t\"disableTlsValidation\": false,\n\t\"updateInterval\": \"2h\",\n\t\"maxIocAge\": \"30d\",\n\t\"allowTxtBlockingReport\": true,\n\t\"paginationLimit\": 5000,\n\t\"addExtendedDnsError\": true\n}\n"
  },
  {
    "path": "Apps/NoDataApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace NoData\n{\n    public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            //do nothing\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n            JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n            foreach (JsonElement jsonBlockedType in jsonAppRecordData.GetProperty(\"blockedTypes\").EnumerateArray())\n            {\n                DnsResourceRecordType blockedType = Enum.Parse<DnsResourceRecordType>(jsonBlockedType.GetString(), true);\n                if ((blockedType == question.Type) || (blockedType == DnsResourceRecordType.ANY))\n                    return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question));\n            }\n\n            return Task.FromResult<DnsDatagram>(null);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Returns a NO DATA response for requests that query for the blocked resource record types in Conditional Forwarder zones.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"blockedTypes\"\": [\n    \"\"A\"\", \n    \"\"AAAA\"\",\n    \"\"ANY\"\"\n  ]\n}\";\n            }\n        }\n\n        #endregion\n    }\n}"
  },
  {
    "path": "Apps/NoDataApp/NoDataApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Version>5.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>NoDataApp</AssemblyName>\n    <RootNamespace>NoData</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <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.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/NoDataApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/NxDomainApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace NxDomain\n{\n    public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference\n    {\n        #region variables\n\n        byte _appPreference;\n\n        IDnsServer _dnsServer;\n        DnsSOARecordData _soaRecord;\n\n        bool _enableBlocking;\n        bool _allowTxtBlockingReport;\n\n        Dictionary<string, object> _blockListZone;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region private\n\n        private static string GetParentZone(string domain)\n        {\n            int i = domain.IndexOf('.');\n            if (i > -1)\n                return domain.Substring(i + 1);\n\n            //dont return root zone\n            return null;\n        }\n\n        private bool IsZoneBlocked(string domain, out string blockedDomain)\n        {\n            domain = domain.ToLowerInvariant();\n\n            do\n            {\n                if (_blockListZone.TryGetValue(domain, out _))\n                {\n                    //found zone blocked\n                    blockedDomain = domain;\n                    return true;\n                }\n\n                domain = GetParentZone(domain);\n            }\n            while (domain is not null);\n\n            blockedDomain = null;\n            return false;\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n            _soaRecord = new DnsSOARecordData(dnsServer.ServerDomain, dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, 60);\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue(\"appPreference\", 20));\n\n            _enableBlocking = jsonConfig.GetProperty(\"enableBlocking\").GetBoolean();\n            _allowTxtBlockingReport = jsonConfig.GetProperty(\"allowTxtBlockingReport\").GetBoolean();\n            _blockListZone = jsonConfig.ReadArrayAsMap(\"blocked\", delegate (JsonElement jsonDomainName) { return new Tuple<string, object>(jsonDomainName.GetString(), null); });\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)\n        {\n            if (!_enableBlocking)\n                return Task.FromResult<DnsDatagram>(null);\n\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!IsZoneBlocked(question.Name, out string blockedDomain))\n                return Task.FromResult<DnsDatagram>(null);\n\n            if (_allowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT))\n            {\n                //return meta data\n                DnsResourceRecord[] answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData(\"source=nx-domain-app; domain=\" + blockedDomain))];\n\n                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 });\n            }\n            else\n            {\n                EDnsOption[] options = null;\n\n                if (_allowTxtBlockingReport && (request.EDNS is not null))\n                    options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, \"source=nx-domain-app; domain=\" + blockedDomain))];\n\n                string parentDomain = GetParentZone(blockedDomain);\n                if (parentDomain is null)\n                    parentDomain = string.Empty;\n\n                IReadOnlyList<DnsResourceRecord> authority = [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord)];\n\n                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 });\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Blocks configured domain names with a NX Domain response.\"; } }\n\n        public byte Preference\n        { get { return _appPreference; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/NxDomainApp/NxDomainApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Version>7.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>NxDomainApp</AssemblyName>\n    <RootNamespace>NxDomain</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <Description>Blocks configured domain names with a NX Domain response.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/NxDomainApp/dnsApp.config",
    "content": "{\n  \"appPreference\": 20,\n  \"enableBlocking\": true,\n  \"allowTxtBlockingReport\": true,\n  \"blocked\": [\n    \"use-application-dns.net\",\n    \"mask.icloud.com\",\n    \"mask-h2.icloud.com\"\n  ]\n}\n"
  },
  {
    "path": "Apps/NxDomainOverrideApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace NxDomainOverride\n{\n    public sealed class App : IDnsApplication, IDnsPostProcessor\n    {\n        #region variables\n\n        bool _enableOverride;\n        uint _defaultTtl;\n        Dictionary<string, string[]> _domainSetMap;\n        Dictionary<string, Set> _sets;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region private\n\n        private static string GetParentZone(string domain)\n        {\n            int i = domain.IndexOf('.');\n            if (i > -1)\n                return domain.Substring(i + 1);\n\n            //dont return root zone\n            return null;\n        }\n\n        private bool TryGetMappedSets(string domain, out string[] setNames)\n        {\n            domain = domain.ToLowerInvariant();\n\n            string parent;\n\n            do\n            {\n                if (_domainSetMap.TryGetValue(domain, out setNames))\n                    return true;\n\n                parent = GetParentZone(domain);\n                if (parent is null)\n                {\n                    if (_domainSetMap.TryGetValue(\"*\", out setNames))\n                        return true;\n\n                    break;\n                }\n\n                domain = \"*.\" + parent;\n\n                if (_domainSetMap.TryGetValue(domain, out setNames))\n                    return true;\n\n                domain = parent;\n            }\n            while (true);\n\n            return false;\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _enableOverride = jsonConfig.GetPropertyValue(\"enableOverride\", true);\n            _defaultTtl = jsonConfig.GetPropertyValue(\"defaultTtl\", 300u);\n\n            _domainSetMap = jsonConfig.ReadObjectAsMap(\"domainSetMap\", delegate (string domain, JsonElement jsonSets)\n            {\n                string[] sets = jsonSets.GetArray();\n\n                return new Tuple<string, string[]>(domain.ToLowerInvariant(), sets);\n            });\n\n            _sets = jsonConfig.ReadArrayAsMap(\"sets\", delegate (JsonElement jsonSet)\n            {\n                Set set = new Set(jsonSet);\n\n                return new Tuple<string, Set>(set.Name, set);\n            });\n\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (!_enableOverride)\n                return Task.FromResult(response);\n\n            if (response.DnssecOk)\n                return Task.FromResult(response);\n\n            if (response.OPCODE != DnsOpcode.StandardQuery)\n                return Task.FromResult(response);\n\n            if (response.RCODE != DnsResponseCode.NxDomain)\n                return Task.FromResult(response);\n\n            DnsQuestionRecord question = request.Question[0];\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    break;\n\n                default:\n                    //NO DATA response\n                    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 });\n            }\n\n            string nxDomain = question.Name;\n\n            foreach (DnsResourceRecord record in response.Answer)\n            {\n                if (record.Type == DnsResourceRecordType.CNAME)\n                    nxDomain = (record.RDATA as DnsCNAMERecordData).Domain;\n            }\n\n            if (!TryGetMappedSets(nxDomain, out string[] setNames))\n                return Task.FromResult(response);\n\n            List<DnsResourceRecord> newAnswer = new List<DnsResourceRecord>();\n            newAnswer.AddRange(response.Answer);\n\n            foreach (string setName in setNames)\n            {\n                if (_sets.TryGetValue(setName, out Set set))\n                {\n                    switch (question.Type)\n                    {\n                        case DnsResourceRecordType.A:\n                            foreach (DnsResourceRecordData rdata in set.RecordDataAddresses)\n                            {\n                                if (rdata is DnsARecordData)\n                                    newAnswer.Add(new DnsResourceRecord(nxDomain, DnsResourceRecordType.A, DnsClass.IN, _defaultTtl, rdata));\n                            }\n                            break;\n\n                        case DnsResourceRecordType.AAAA:\n                            foreach (DnsResourceRecordData rdata in set.RecordDataAddresses)\n                            {\n                                if (rdata is DnsAAAARecordData)\n                                    newAnswer.Add(new DnsResourceRecord(nxDomain, DnsResourceRecordType.AAAA, DnsClass.IN, _defaultTtl, rdata));\n                            }\n                            break;\n\n                        default:\n                            throw new InvalidOperationException();\n                    }\n                }\n            }\n\n            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 });\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Overrides NX Domain response with custom A/AAAA record response for configured domain names.\"; } }\n\n        #endregion\n\n        class Set\n        {\n            #region variables\n\n            readonly string _name;\n            readonly DnsResourceRecordData[] _rdataAddresses;\n\n            #endregion\n\n            #region constructor\n\n            public Set(JsonElement jsonSet)\n            {\n                _name = jsonSet.GetProperty(\"name\").GetString();\n                _rdataAddresses = jsonSet.ReadArray<DnsResourceRecordData>(\"addresses\", delegate (string item)\n                {\n                    IPAddress address = IPAddress.Parse(item);\n\n                    switch (address.AddressFamily)\n                    {\n                        case AddressFamily.InterNetwork:\n                            return new DnsARecordData(address);\n\n                        case AddressFamily.InterNetworkV6:\n                            return new DnsAAAARecordData(address);\n\n                        default:\n                            throw new NotSupportedException(\"Address family not supported: \" + address.AddressFamily.ToString());\n                    }\n                });\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public DnsResourceRecordData[] RecordDataAddresses\n            { get { return _rdataAddresses; } }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/NxDomainOverrideApp/NxDomainOverrideApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Version>3.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>NxDomainOverrideApp</AssemblyName>\n    <RootNamespace>NxDomainOverride</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <Description>Overrides NX Domain response with custom A/AAAA record response for configured domain names.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/NxDomainOverrideApp/dnsApp.config",
    "content": "{\n  \"enableOverride\": true,\n  \"defaultTtl\": 300,\n  \"domainSetMap\": {\n    \"*\": [\"set1\"],\n    \"example.com\": [\"set1\", \"set2\"]\n  },\n  \"sets\": [\n    {\n      \"name\": \"set1\",\n      \"addresses\": [\n        \"192.168.10.1\"\n      ]\n    },\n    {\n      \"name\": \"set2\",\n      \"addresses\": [\n        \"1.2.3.4\",\n        \"5.6.7.8\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "Apps/QueryLogsMySqlApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing MySqlConnector;\nusing System;\nusing System.Collections.Generic;\nusing System.Data.Common;\nusing System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Channels;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace QueryLogsMySql\n{\n    public sealed class App : IDnsApplication, IDnsQueryLogger, IDnsQueryLogs\n    {\n        #region variables\n\n        IDnsServer? _dnsServer;\n\n        bool _enableLogging;\n        int _maxQueueSize;\n        int _maxLogDays;\n        int _maxLogRecords;\n        string? _databaseName;\n        string? _connectionString;\n\n        Channel<LogEntry>? _channel;\n        ChannelWriter<LogEntry>? _channelWriter;\n        Thread? _consumerThread;\n        const int BULK_INSERT_COUNT = 1000;\n        const int BULK_INSERT_ERROR_DELAY = 10000;\n\n        readonly Timer _cleanupTimer;\n        const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000;\n        const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000;\n\n        bool _isStartupInit = true;\n\n        #endregion\n\n        #region constructor\n\n        public App()\n        {\n            _cleanupTimer = new Timer(async delegate (object? state)\n            {\n                try\n                {\n                    await using (MySqlConnection connection = new MySqlConnection(_connectionString + $\" Database={_databaseName};\"))\n                    {\n                        await connection.OpenAsync();\n\n                        if (_maxLogRecords > 0)\n                        {\n                            int totalRecords;\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"SELECT Count(*) FROM dns_logs;\";\n\n                                totalRecords = Convert.ToInt32(await command.ExecuteScalarAsync() ?? 0);\n                            }\n\n                            int recordsToRemove = totalRecords - _maxLogRecords;\n                            if (recordsToRemove > 0)\n                            {\n                                await using (MySqlCommand command = connection.CreateCommand())\n                                {\n                                    command.CommandText = $\"DELETE FROM dns_logs WHERE dlid IN (SELECT * FROM (SELECT dlid FROM dns_logs ORDER BY dlid LIMIT {recordsToRemove}) AS T1);\";\n\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                            }\n                        }\n\n                        if (_maxLogDays > 0)\n                        {\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"DELETE FROM dns_logs WHERE timestamp < @timestamp;\";\n\n                                command.Parameters.AddWithValue(\"@timestamp\", DateTime.UtcNow.AddDays(_maxLogDays * -1));\n\n                                await command.ExecuteNonQueryAsync();\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer?.WriteLog(ex);\n                }\n                finally\n                {\n                    try\n                    {\n                        _cleanupTimer?.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);\n                    }\n                    catch (ObjectDisposedException)\n                    { }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _enableLogging = false; //turn off logging\n\n            _cleanupTimer?.Dispose();\n\n            StopChannel();\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private void StartNewChannel(int maxQueueSize)\n        {\n            ChannelWriter<LogEntry>? existingChannelWriter = _channelWriter;\n\n            //start new channel and consumer thread\n            BoundedChannelOptions options = new BoundedChannelOptions(maxQueueSize);\n            options.SingleWriter = true;\n            options.SingleReader = true;\n            options.FullMode = BoundedChannelFullMode.DropWrite;\n\n            _channel = Channel.CreateBounded<LogEntry>(options);\n            _channelWriter = _channel.Writer;\n            ChannelReader<LogEntry> channelReader = _channel.Reader;\n\n            _consumerThread = new Thread(async delegate ()\n            {\n                try\n                {\n                    List<LogEntry> logs = new List<LogEntry>(BULK_INSERT_COUNT);\n                    StringBuilder sb = new StringBuilder(4096);\n\n                    while (!_disposed && await channelReader.WaitToReadAsync())\n                    {\n                        while (!_disposed && (logs.Count < BULK_INSERT_COUNT) && channelReader.TryRead(out LogEntry log))\n                        {\n                            logs.Add(log);\n                        }\n\n                        if (logs.Count < 1)\n                            continue;\n\n                        await BulkInsertLogsAsync(logs, sb);\n\n                        logs.Clear();\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer?.WriteLog(ex);\n                }\n            });\n\n            _consumerThread.Name = GetType().Name;\n            _consumerThread.IsBackground = true;\n            _consumerThread.Start();\n\n            //complete old channel to stop its consumer thread\n            existingChannelWriter?.TryComplete();\n        }\n\n        private void StopChannel()\n        {\n            _channel?.Writer.TryComplete();\n        }\n\n        private async Task BulkInsertLogsAsync(List<LogEntry> logs, StringBuilder sb)\n        {\n            try\n            {\n                await using (MySqlConnection connection = new MySqlConnection(_connectionString + $\" Database={_databaseName};\"))\n                {\n                    await connection.OpenAsync();\n\n                    await using (MySqlCommand command = connection.CreateCommand())\n                    {\n                        sb.Length = 0;\n                        sb.Append(\"INSERT INTO dns_logs (server, timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer) VALUES \");\n\n                        for (int i = 0; i < logs.Count; i++)\n                        {\n                            if (i == 0)\n                                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})\");\n                            else\n                                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})\");\n                        }\n                        command.CommandText = sb.ToString();\n\n                        for (int i = 0; i < logs.Count; i++)\n                        {\n                            LogEntry log = logs[i];\n\n                            MySqlParameter paramServer = command.Parameters.Add(\"@server\" + i, MySqlDbType.VarChar);\n                            MySqlParameter paramTimestamp = command.Parameters.Add(\"@timestamp\" + i, MySqlDbType.DateTime);\n                            MySqlParameter paramClientIp = command.Parameters.Add(\"@client_ip\" + i, MySqlDbType.VarChar);\n                            MySqlParameter paramProtocol = command.Parameters.Add(\"@protocol\" + i, MySqlDbType.Byte);\n                            MySqlParameter paramResponseType = command.Parameters.Add(\"@response_type\" + i, MySqlDbType.Byte);\n                            MySqlParameter paramResponseRtt = command.Parameters.Add(\"@response_rtt\" + i, MySqlDbType.Double);\n                            MySqlParameter paramRcode = command.Parameters.Add(\"@rcode\" + i, MySqlDbType.Byte);\n                            MySqlParameter paramQname = command.Parameters.Add(\"@qname\" + i, MySqlDbType.VarChar);\n                            MySqlParameter paramQtype = command.Parameters.Add(\"@qtype\" + i, MySqlDbType.Int16);\n                            MySqlParameter paramQclass = command.Parameters.Add(\"@qclass\" + i, MySqlDbType.Int16);\n                            MySqlParameter paramAnswer = command.Parameters.Add(\"@answer\" + i, MySqlDbType.VarChar);\n\n                            paramServer.Value = _dnsServer?.ServerDomain;\n                            paramTimestamp.Value = log.Timestamp;\n                            paramClientIp.Value = log.RemoteEP.Address.ToString();\n                            paramProtocol.Value = (byte)log.Protocol;\n\n                            DnsServerResponseType responseType;\n\n                            if (log.Response.Tag == null)\n                                responseType = DnsServerResponseType.Recursive;\n                            else\n                                responseType = (DnsServerResponseType)log.Response.Tag;\n\n                            paramResponseType.Value = (byte)responseType;\n\n                            if ((responseType == DnsServerResponseType.Recursive) && (log.Response.Metadata is not null))\n                                paramResponseRtt.Value = log.Response.Metadata.RoundTripTime;\n                            else\n                                paramResponseRtt.Value = DBNull.Value;\n\n                            paramRcode.Value = (byte)log.Response.RCODE;\n\n                            if (log.Request.Question.Count > 0)\n                            {\n                                DnsQuestionRecord query = log.Request.Question[0];\n\n                                paramQname.Value = query.Name.ToLowerInvariant();\n                                paramQtype.Value = (short)query.Type;\n                                paramQclass.Value = (short)query.Class;\n                            }\n                            else\n                            {\n                                paramQname.Value = DBNull.Value;\n                                paramQtype.Value = DBNull.Value;\n                                paramQclass.Value = DBNull.Value;\n                            }\n\n                            if (log.Response.Answer.Count == 0)\n                            {\n                                paramAnswer.Value = DBNull.Value;\n                            }\n                            else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer)\n                            {\n                                paramAnswer.Value = \"[ZONE TRANSFER]\";\n                            }\n                            else\n                            {\n                                string? answer = null;\n\n                                foreach (DnsResourceRecord record in log.Response.Answer)\n                                {\n                                    if (answer is null)\n                                        answer = record.Type.ToString() + \" \" + record.RDATA.ToString();\n                                    else\n                                        answer += \", \" + record.Type.ToString() + \" \" + record.RDATA.ToString();\n                                }\n\n                                if (answer?.Length > 4000)\n                                    answer = answer.Substring(0, 4000);\n\n                                paramAnswer.Value = answer;\n                            }\n                        }\n\n                        await command.ExecuteNonQueryAsync();\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer?.WriteLog(ex);\n\n                await Task.Delay(BULK_INSERT_ERROR_DELAY);\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            try\n            {\n                _dnsServer = dnsServer;\n\n                using JsonDocument jsonDocument = JsonDocument.Parse(config);\n                JsonElement jsonConfig = jsonDocument.RootElement;\n\n                bool enableLogging = jsonConfig.GetPropertyValue(\"enableLogging\", false);\n                int maxQueueSize = jsonConfig.GetPropertyValue(\"maxQueueSize\", 1000000);\n                _maxLogDays = jsonConfig.GetPropertyValue(\"maxLogDays\", 0);\n                _maxLogRecords = jsonConfig.GetPropertyValue(\"maxLogRecords\", 0);\n                _databaseName = jsonConfig.GetPropertyValue(\"databaseName\", \"DnsQueryLogs\");\n                _connectionString = jsonConfig.GetPropertyValue(\"connectionString\", null);\n\n                if (_connectionString is null)\n                    throw new Exception(\"Please specify a valid connection string in 'connectionString' parameter.\");\n\n                if (_connectionString.Replace(\" \", \"\").Contains(\"Database=\", StringComparison.OrdinalIgnoreCase))\n                    throw new Exception(\"The 'connectionString' parameter must not define 'Database'. Configure the 'databaseName' parameter above instead.\");\n\n                if (!_connectionString.TrimEnd().EndsWith(';'))\n                    _connectionString += \";\";\n\n                async Task ApplyConfig()\n                {\n                    if (enableLogging)\n                    {\n                        await using (MySqlConnection connection = new MySqlConnection(_connectionString))\n                        {\n                            await connection.OpenAsync();\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = @$\"\nCREATE DATABASE IF NOT EXISTS `{_databaseName}`;\n\nUSE `{_databaseName}`;\n\nCREATE TABLE IF NOT EXISTS dns_logs\n(\n    dlid INT PRIMARY KEY AUTO_INCREMENT,\n    server varchar(255),\n    timestamp DATETIME NOT NULL,\n    client_ip VARCHAR(39) NOT NULL,\n    protocol TINYINT UNSIGNED NOT NULL,\n    response_type TINYINT NOT NULL,\n    response_rtt REAL,\n    rcode TINYINT NOT NULL,\n    qname VARCHAR(255),\n    qtype SMALLINT,\n    qclass SMALLINT,\n    answer VARCHAR(4000)\n);\n\";\n\n                                await command.ExecuteNonQueryAsync();\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"ALTER TABLE dns_logs ADD server varchar(255);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"ALTER TABLE dns_logs MODIFY protocol TINYINT UNSIGNED NOT NULL;\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_server ON dns_logs (server);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_timestamp ON dns_logs (timestamp);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_client_ip ON dns_logs (client_ip);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_protocol ON dns_logs (protocol);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_response_type ON dns_logs (response_type);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_rcode ON dns_logs (rcode);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_qname ON dns_logs (qname)\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_qtype ON dns_logs (qtype);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_qclass ON dns_logs (qclass);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_timestamp_client_ip ON dns_logs (timestamp, client_ip);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_timestamp_qname ON dns_logs (timestamp, qname);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_client_qname ON dns_logs (client_ip, qname);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_query ON dns_logs (qname, qtype);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n\n                            await using (MySqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"CREATE INDEX index_all ON dns_logs (server, timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass);\";\n\n                                try\n                                {\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                                catch\n                                { }\n                            }\n                        }\n\n                        if (!_enableLogging || (_maxQueueSize != maxQueueSize))\n                            StartNewChannel(maxQueueSize);\n                    }\n                    else\n                    {\n                        StopChannel();\n                    }\n\n                    _enableLogging = enableLogging;\n                    _maxQueueSize = maxQueueSize;\n\n                    if ((_maxLogDays > 0) || (_maxLogRecords > 0))\n                        _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                    else\n                        _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n\n                if (_isStartupInit)\n                {\n                    //this is the first time this app is being initialized\n                    ThreadPool.QueueUserWorkItem(async delegate (object? state)\n                    {\n                        try\n                        {\n                            const int MAX_RETRIES = 20;\n                            const int RETRY_DELAY = 30000; //30 seconds\n                            int retryCount = 0;\n\n                            while (true)\n                            {\n                                try\n                                {\n                                    await ApplyConfig();\n                                    return;\n                                }\n                                catch (Exception ex)\n                                {\n                                    if (ex is not MySqlException ex2)\n                                    {\n                                        _dnsServer?.WriteLog(ex);\n                                        return;\n                                    }\n\n                                    switch (ex2.ErrorCode)\n                                    {\n                                        case MySqlErrorCode.UnableToConnectToHost:\n                                        case MySqlErrorCode.TooManyUserConnections:\n                                            retryCount++;\n\n                                            if (retryCount < MAX_RETRIES)\n                                            {\n                                                _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})\");\n                                                _dnsServer?.WriteLog(ex);\n\n                                                await Task.Delay(RETRY_DELAY);\n                                            }\n                                            else\n                                            {\n                                                _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.\");\n                                                _dnsServer?.WriteLog(ex);\n                                                return;\n                                            }\n                                            break;\n\n                                        default:\n                                            _dnsServer?.WriteLog($\"Failed to connect to the database server ({ex2.ErrorCode}). Please check the app config and make sure the database server is online.\");\n                                            _dnsServer?.WriteLog(ex);\n                                            return;\n                                    }\n                                }\n                            }\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsServer?.WriteLog(ex);\n                        }\n                    });\n                }\n                else\n                {\n                    //init via API call\n                    await ApplyConfig();\n                }\n            }\n            finally\n            {\n                _isStartupInit = false; //reset flag\n            }\n        }\n\n        public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (_enableLogging)\n                _channelWriter?.TryWrite(new LogEntry(timestamp, request, remoteEP, protocol, response));\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsLogPage> 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)\n        {\n            if (pageNumber == 0)\n                pageNumber = 1;\n\n            if (qname is not null)\n                qname = qname.ToLowerInvariant();\n\n            string whereClause = $\"server = '{_dnsServer?.ServerDomain}' AND \";\n\n            if (start is not null)\n                whereClause += \"timestamp >= @start AND \";\n\n            if (end is not null)\n                whereClause += \"timestamp <= @end AND \";\n\n            if (clientIpAddress is not null)\n                whereClause += \"client_ip = @client_ip AND \";\n\n            if (protocol is not null)\n                whereClause += \"protocol = @protocol AND \";\n\n            if (responseType is not null)\n                whereClause += \"response_type = @response_type AND \";\n\n            if (rcode is not null)\n                whereClause += \"rcode = @rcode AND \";\n\n            if (qname is not null)\n            {\n                if (qname.Contains('*'))\n                {\n                    whereClause += \"qname like @qname AND \";\n                    qname = qname.Replace(\"*\", \"%\");\n                }\n                else\n                {\n                    whereClause += \"qname = @qname AND \";\n                }\n            }\n\n            if (qtype is not null)\n                whereClause += \"qtype = @qtype AND \";\n\n            if (qclass is not null)\n                whereClause += \"qclass = @qclass AND \";\n            if (!string.IsNullOrEmpty(whereClause))\n                whereClause = whereClause.Substring(0, whereClause.Length - 5);\n\n            await using (MySqlConnection connection = new MySqlConnection(_connectionString + $\" Database={_databaseName};\"))\n            {\n                await connection.OpenAsync();\n\n                //find total entries\n                long totalEntries;\n\n                await using (MySqlCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"SELECT Count(*) FROM dns_logs\" + (string.IsNullOrEmpty(whereClause) ? \";\" : \" WHERE \" + whereClause + \";\");\n\n                    if (start is not null)\n                        command.Parameters.AddWithValue(\"@start\", start);\n\n                    if (end is not null)\n                        command.Parameters.AddWithValue(\"@end\", end);\n\n                    if (clientIpAddress is not null)\n                        command.Parameters.AddWithValue(\"@client_ip\", clientIpAddress.ToString());\n\n                    if (protocol is not null)\n                        command.Parameters.AddWithValue(\"@protocol\", (byte)protocol);\n\n                    if (responseType is not null)\n                        command.Parameters.AddWithValue(\"@response_type\", (byte)responseType);\n\n                    if (rcode is not null)\n                        command.Parameters.AddWithValue(\"@rcode\", (byte)rcode);\n\n                    if (qname is not null)\n                        command.Parameters.AddWithValue(\"@qname\", qname);\n\n                    if (qtype is not null)\n                        command.Parameters.AddWithValue(\"@qtype\", (short)qtype);\n\n                    if (qclass is not null)\n                        command.Parameters.AddWithValue(\"@qclass\", (short)qclass);\n\n                    totalEntries = Convert.ToInt64(await command.ExecuteScalarAsync() ?? 0L);\n                }\n\n                long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0);\n\n                if ((pageNumber > totalPages) || (pageNumber < 0))\n                    pageNumber = totalPages;\n\n                long endRowNum;\n                long startRowNum;\n\n                if (descendingOrder)\n                {\n                    endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage);\n                    startRowNum = endRowNum - entriesPerPage;\n                }\n                else\n                {\n                    endRowNum = pageNumber * entriesPerPage;\n                    startRowNum = endRowNum - entriesPerPage;\n                }\n\n                List<DnsLogEntry> entries = new List<DnsLogEntry>(entriesPerPage);\n\n                await using (MySqlCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = @\"\nSELECT * FROM (\n    SELECT\n        ROW_NUMBER() OVER ( \n            ORDER BY dlid\n        ) row_num,\n        timestamp,\n        client_ip,\n        protocol,\n        response_type,\n        response_rtt,\n        rcode,\n        qname,\n        qtype,\n        qclass,\n        answer\n    FROM\n        dns_logs\n\" + (string.IsNullOrEmpty(whereClause) ? \"\" : \"WHERE \" + whereClause) + @\"\n) t\nWHERE \n    row_num > @start_row_num AND row_num <= @end_row_num\nORDER BY row_num\" + (descendingOrder ? \" DESC\" : \"\");\n\n                    command.Parameters.AddWithValue(\"@start_row_num\", startRowNum);\n                    command.Parameters.AddWithValue(\"@end_row_num\", endRowNum);\n\n                    if (start is not null)\n                        command.Parameters.AddWithValue(\"@start\", start);\n\n                    if (end is not null)\n                        command.Parameters.AddWithValue(\"@end\", end);\n\n                    if (clientIpAddress is not null)\n                        command.Parameters.AddWithValue(\"@client_ip\", clientIpAddress.ToString());\n\n                    if (protocol is not null)\n                        command.Parameters.AddWithValue(\"@protocol\", (byte)protocol);\n\n                    if (responseType is not null)\n                        command.Parameters.AddWithValue(\"@response_type\", (byte)responseType);\n\n                    if (rcode is not null)\n                        command.Parameters.AddWithValue(\"@rcode\", (byte)rcode);\n\n                    if (qname is not null)\n                        command.Parameters.AddWithValue(\"@qname\", qname);\n\n                    if (qtype is not null)\n                        command.Parameters.AddWithValue(\"@qtype\", (short)qtype);\n\n                    if (qclass is not null)\n                        command.Parameters.AddWithValue(\"@qclass\", (short)qclass);\n\n                    await using (DbDataReader reader = await command.ExecuteReaderAsync())\n                    {\n                        while (await reader.ReadAsync())\n                        {\n                            double? responseRtt;\n\n                            if (reader.IsDBNull(5))\n                                responseRtt = null;\n                            else\n                                responseRtt = reader.GetFloat(5);\n\n                            DnsQuestionRecord? question;\n\n                            if (reader.IsDBNull(7))\n                                question = null;\n                            else\n                                question = new DnsQuestionRecord(reader.GetString(7), (DnsResourceRecordType)reader.GetInt16(8), (DnsClass)reader.GetInt16(9), false);\n\n                            string? answer;\n\n                            if (reader.IsDBNull(10))\n                                answer = null;\n                            else\n                                answer = reader.GetString(10);\n\n                            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));\n                        }\n                    }\n                }\n\n                return new DnsLogPage(pageNumber, totalPages, totalEntries, entries);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Logs all incoming DNS requests and their responses in a MySQL database that can be queried from the DNS Server web console.\"; } }\n\n        #endregion\n\n        readonly struct LogEntry\n        {\n            #region variables\n\n            public readonly DateTime Timestamp;\n            public readonly DnsDatagram Request;\n            public readonly IPEndPoint RemoteEP;\n            public readonly DnsTransportProtocol Protocol;\n            public readonly DnsDatagram Response;\n\n            #endregion\n\n            #region constructor\n\n            public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n            {\n                Timestamp = timestamp;\n                Request = request;\n                RemoteEP = remoteEP;\n                Protocol = protocol;\n                Response = response;\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/QueryLogsMySqlApp/QueryLogsMySqlApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<Version>3.0.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>QueryLogsMySqlApp</AssemblyName>\n\t\t<RootNamespace>QueryLogsMySql</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t\t<Nullable>enable</Nullable>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"MySqlConnector\" Version=\"2.5.0\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>True</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/QueryLogsMySqlApp/dnsApp.config",
    "content": "{\n  \"enableLogging\": false,\n  \"maxQueueSize\": 1000000,\n  \"maxLogDays\": 0,\n  \"maxLogRecords\": 0,\n  \"databaseName\": \"DnsQueryLogs\",\n  \"connectionString\": \"Server=192.168.180.128; Port=3306; Uid=username; Pwd=password;\"\n}\n"
  },
  {
    "path": "Apps/QueryLogsSqlServerApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing Microsoft.Data.SqlClient;\nusing System;\nusing System.Collections.Generic;\nusing System.Data;\nusing System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Channels;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace QueryLogsSqlServer\n{\n    public sealed class App : IDnsApplication, IDnsQueryLogger, IDnsQueryLogs\n    {\n        #region variables\n\n        IDnsServer? _dnsServer;\n\n        bool _enableLogging;\n        int _maxQueueSize;\n        int _maxLogDays;\n        int _maxLogRecords;\n        string? _databaseName;\n        string? _connectionString;\n\n        Channel<LogEntry>? _channel;\n        ChannelWriter<LogEntry>? _channelWriter;\n        Thread? _consumerThread;\n        const int BULK_INSERT_COUNT = 190; //sql server supports a maximum of 2100 parameters per query\n        const int BULK_INSERT_ERROR_DELAY = 10000;\n\n        readonly Timer _cleanupTimer;\n        const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000;\n        const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000;\n\n        bool _isStartupInit = true;\n\n        #endregion\n\n        #region constructor\n\n        public App()\n        {\n            _cleanupTimer = new Timer(async delegate (object? state)\n            {\n                try\n                {\n                    await using (SqlConnection connection = new SqlConnection(_connectionString + $\" Initial Catalog={_databaseName};\"))\n                    {\n                        await connection.OpenAsync();\n\n                        if (_maxLogRecords > 0)\n                        {\n                            int totalRecords;\n\n                            await using (SqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"SELECT Count(*) FROM dns_logs;\";\n\n                                totalRecords = Convert.ToInt32(await command.ExecuteScalarAsync() ?? 0);\n                            }\n\n                            int recordsToRemove = totalRecords - _maxLogRecords;\n                            if (recordsToRemove > 0)\n                            {\n                                await using (SqlCommand command = connection.CreateCommand())\n                                {\n                                    command.CommandText = $\"DELETE FROM dns_logs WHERE dlid IN (SELECT TOP {recordsToRemove} dlid FROM dns_logs ORDER BY dlid);\";\n\n                                    await command.ExecuteNonQueryAsync();\n                                }\n                            }\n                        }\n\n                        if (_maxLogDays > 0)\n                        {\n                            await using (SqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"DELETE FROM dns_logs WHERE timestamp < @timestamp;\";\n\n                                command.Parameters.AddWithValue(\"@timestamp\", DateTime.UtcNow.AddDays(_maxLogDays * -1));\n\n                                await command.ExecuteNonQueryAsync();\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer?.WriteLog(ex);\n                }\n                finally\n                {\n                    try\n                    {\n                        _cleanupTimer?.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);\n                    }\n                    catch (ObjectDisposedException)\n                    { }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _enableLogging = false; //turn off logging\n\n            _cleanupTimer?.Dispose();\n\n            StopChannel();\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private void StartNewChannel(int maxQueueSize)\n        {\n            ChannelWriter<LogEntry>? existingChannelWriter = _channelWriter;\n\n            //start new channel and consumer thread\n            BoundedChannelOptions options = new BoundedChannelOptions(maxQueueSize);\n            options.SingleWriter = true;\n            options.SingleReader = true;\n            options.FullMode = BoundedChannelFullMode.DropWrite;\n\n            _channel = Channel.CreateBounded<LogEntry>(options);\n            _channelWriter = _channel.Writer;\n            ChannelReader<LogEntry> channelReader = _channel.Reader;\n\n            _consumerThread = new Thread(async delegate ()\n            {\n                try\n                {\n                    List<LogEntry> logs = new List<LogEntry>(BULK_INSERT_COUNT);\n                    StringBuilder sb = new StringBuilder(4096);\n\n                    while (!_disposed && await channelReader.WaitToReadAsync())\n                    {\n                        while (!_disposed && (logs.Count < BULK_INSERT_COUNT) && channelReader.TryRead(out LogEntry log))\n                        {\n                            logs.Add(log);\n                        }\n\n                        if (logs.Count < 1)\n                            continue;\n\n                        await BulkInsertLogsAsync(logs, sb);\n\n                        logs.Clear();\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer?.WriteLog(ex);\n                }\n            });\n\n            _consumerThread.Name = GetType().Name;\n            _consumerThread.IsBackground = true;\n            _consumerThread.Start();\n\n            //complete old channel to stop its consumer thread\n            existingChannelWriter?.TryComplete();\n        }\n\n        private void StopChannel()\n        {\n            _channel?.Writer.TryComplete();\n        }\n\n        private async Task BulkInsertLogsAsync(List<LogEntry> logs, StringBuilder sb)\n        {\n            try\n            {\n                await using (SqlConnection connection = new SqlConnection(_connectionString + $\" Initial Catalog={_databaseName};\"))\n                {\n                    await connection.OpenAsync();\n\n                    await using (SqlCommand command = connection.CreateCommand())\n                    {\n                        sb.Length = 0;\n                        sb.Append(\"INSERT INTO dns_logs (server, timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer) VALUES \");\n\n                        for (int i = 0; i < logs.Count; i++)\n                        {\n                            if (i == 0)\n                                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})\");\n                            else\n                                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})\");\n                        }\n\n                        command.CommandText = sb.ToString();\n\n                        for (int i = 0; i < logs.Count; i++)\n                        {\n                            LogEntry log = logs[i];\n\n                            SqlParameter paramServer = command.Parameters.Add(\"@server\" + i, SqlDbType.VarChar);\n                            SqlParameter paramTimestamp = command.Parameters.Add(\"@timestamp\" + i, SqlDbType.DateTime);\n                            SqlParameter paramClientIp = command.Parameters.Add(\"@client_ip\" + i, SqlDbType.VarChar);\n                            SqlParameter paramProtocol = command.Parameters.Add(\"@protocol\" + i, SqlDbType.TinyInt);\n                            SqlParameter paramResponseType = command.Parameters.Add(\"@response_type\" + i, SqlDbType.TinyInt);\n                            SqlParameter paramResponseRtt = command.Parameters.Add(\"@response_rtt\" + i, SqlDbType.Real);\n                            SqlParameter paramRcode = command.Parameters.Add(\"@rcode\" + i, SqlDbType.TinyInt);\n                            SqlParameter paramQname = command.Parameters.Add(\"@qname\" + i, SqlDbType.VarChar);\n                            SqlParameter paramQtype = command.Parameters.Add(\"@qtype\" + i, SqlDbType.SmallInt);\n                            SqlParameter paramQclass = command.Parameters.Add(\"@qclass\" + i, SqlDbType.SmallInt);\n                            SqlParameter paramAnswer = command.Parameters.Add(\"@answer\" + i, SqlDbType.VarChar);\n\n                            paramServer.Value = _dnsServer?.ServerDomain;\n                            paramTimestamp.Value = log.Timestamp;\n                            paramClientIp.Value = log.RemoteEP.Address.ToString();\n                            paramProtocol.Value = (byte)log.Protocol;\n\n                            DnsServerResponseType responseType;\n\n                            if (log.Response.Tag == null)\n                                responseType = DnsServerResponseType.Recursive;\n                            else\n                                responseType = (DnsServerResponseType)log.Response.Tag;\n\n                            paramResponseType.Value = (byte)responseType;\n\n                            if ((responseType == DnsServerResponseType.Recursive) && (log.Response.Metadata is not null))\n                                paramResponseRtt.Value = log.Response.Metadata.RoundTripTime;\n                            else\n                                paramResponseRtt.Value = DBNull.Value;\n\n                            paramRcode.Value = (byte)log.Response.RCODE;\n\n                            if (log.Request.Question.Count > 0)\n                            {\n                                DnsQuestionRecord query = log.Request.Question[0];\n\n                                paramQname.Value = query.Name.ToLowerInvariant();\n                                paramQtype.Value = (short)query.Type;\n                                paramQclass.Value = (short)query.Class;\n                            }\n                            else\n                            {\n                                paramQname.Value = DBNull.Value;\n                                paramQtype.Value = DBNull.Value;\n                                paramQclass.Value = DBNull.Value;\n                            }\n\n                            if (log.Response.Answer.Count == 0)\n                            {\n                                paramAnswer.Value = DBNull.Value;\n                            }\n                            else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer)\n                            {\n                                paramAnswer.Value = \"[ZONE TRANSFER]\";\n                            }\n                            else\n                            {\n                                string? answer = null;\n\n                                foreach (DnsResourceRecord record in log.Response.Answer)\n                                {\n                                    if (answer is null)\n                                        answer = record.Type.ToString() + \" \" + record.RDATA.ToString();\n                                    else\n                                        answer += \", \" + record.Type.ToString() + \" \" + record.RDATA.ToString();\n                                }\n\n                                if (answer?.Length > 4000)\n                                    answer = answer.Substring(0, 4000);\n\n                                paramAnswer.Value = answer;\n                            }\n                        }\n\n                        await command.ExecuteNonQueryAsync();\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer?.WriteLog(ex);\n\n                await Task.Delay(BULK_INSERT_ERROR_DELAY);\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            try\n            {\n                _dnsServer = dnsServer;\n\n                using JsonDocument jsonDocument = JsonDocument.Parse(config);\n                JsonElement jsonConfig = jsonDocument.RootElement;\n\n                bool enableLogging = jsonConfig.GetPropertyValue(\"enableLogging\", false);\n                int maxQueueSize = jsonConfig.GetPropertyValue(\"maxQueueSize\", 1000000);\n                _maxLogDays = jsonConfig.GetPropertyValue(\"maxLogDays\", 0);\n                _maxLogRecords = jsonConfig.GetPropertyValue(\"maxLogRecords\", 0);\n                _databaseName = jsonConfig.GetPropertyValue(\"databaseName\", \"DnsQueryLogs\");\n                _connectionString = jsonConfig.GetPropertyValue(\"connectionString\", null);\n\n                if (_connectionString is null)\n                    throw new Exception(\"Please specify a valid connection string in 'connectionString' parameter.\");\n\n                if (_connectionString.Contains(\"Initial Catalog\", StringComparison.OrdinalIgnoreCase))\n                    throw new Exception(\"The 'connectionString' parameter must not define 'Initial Catalog'. Configure the 'databaseName' parameter above instead.\");\n\n                if (!_connectionString.TrimEnd().EndsWith(';'))\n                    _connectionString += \";\";\n\n                async Task ApplyConfig()\n                {\n                    if (enableLogging)\n                    {\n                        await using (SqlConnection connection = new SqlConnection(_connectionString))\n                        {\n                            await connection.OpenAsync();\n\n                            await using (SqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = @$\"\nIF NOT EXISTS(SELECT * FROM sys.databases WHERE name = '{_databaseName}')\nBEGIN\n    CREATE DATABASE \"\"{_databaseName}\"\";\nEND\n\";\n\n                                await command.ExecuteNonQueryAsync();\n                            }\n\n                            await using (SqlCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = @$\"\nUSE \"\"{_databaseName}\"\";\n\nIF NOT EXISTS (SELECT * FROM sys.tables WHERE name='dns_logs' and type='U')\nBEGIN\n    CREATE TABLE dns_logs\n    (\n        dlid INT IDENTITY(1,1) PRIMARY KEY,\n        server varchar(255),\n        timestamp DATETIME NOT NULL,\n        client_ip VARCHAR(39) NOT NULL,\n        protocol TINYINT NOT NULL,\n        response_type TINYINT NOT NULL,\n        response_rtt REAL,\n        rcode TINYINT NOT NULL,\n        qname VARCHAR(255),\n        qtype SMALLINT,\n        qclass SMALLINT,\n        answer VARCHAR(4000)\n    );\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.columns WHERE name = 'server' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    ALTER TABLE dns_logs ADD server varchar(255);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_server' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_server ON dns_logs (server);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_timestamp' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_timestamp ON dns_logs (timestamp);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_client_ip' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_client_ip ON dns_logs (client_ip);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_protocol' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_protocol ON dns_logs (protocol);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_response_type' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_response_type ON dns_logs (response_type);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_rcode' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_rcode ON dns_logs (rcode);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_qname' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_qname ON dns_logs (qname)\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_qtype' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_qtype ON dns_logs (qtype);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_qclass' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_qclass ON dns_logs (qclass);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_timestamp_client_ip' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_timestamp_client_ip ON dns_logs (timestamp, client_ip);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_timestamp_qname' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_timestamp_qname ON dns_logs (timestamp, qname);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_client_qname' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_client_qname ON dns_logs (client_ip, qname);\nEND\n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_query' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_query ON dns_logs (qname, qtype);\nEND \n\nIF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_all' AND object_id = OBJECT_ID('dns_logs'))\nBEGIN\n    CREATE INDEX index_all ON dns_logs (server, timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass);\nEND\n\";\n\n                                await command.ExecuteNonQueryAsync();\n                            }\n                        }\n\n                        if (!_enableLogging || (_maxQueueSize != maxQueueSize))\n                            StartNewChannel(maxQueueSize);\n                    }\n                    else\n                    {\n                        StopChannel();\n                    }\n\n                    _enableLogging = enableLogging;\n                    _maxQueueSize = maxQueueSize;\n\n                    if ((_maxLogDays > 0) || (_maxLogRecords > 0))\n                        _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                    else\n                        _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n\n                if (_isStartupInit)\n                {\n                    //this is the first time this app is being initialized\n                    ThreadPool.QueueUserWorkItem(async delegate (object? state)\n                    {\n                        try\n                        {\n                            const int MAX_RETRIES = 20;\n                            const int RETRY_DELAY = 30000; //30 seconds\n                            int retryCount = 0;\n\n                            while (true)\n                            {\n                                try\n                                {\n                                    await ApplyConfig();\n                                    return;\n                                }\n                                catch (Exception ex)\n                                {\n                                    if (ex is not SqlException ex2)\n                                    {\n                                        _dnsServer?.WriteLog(ex);\n                                        return;\n                                    }\n\n                                    switch (ex2.Number)\n                                    {\n                                        case 258:\n                                            retryCount++;\n\n                                            if (retryCount < MAX_RETRIES)\n                                            {\n                                                _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})\");\n                                                _dnsServer?.WriteLog(ex);\n\n                                                await Task.Delay(RETRY_DELAY);\n                                            }\n                                            else\n                                            {\n                                                _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.\");\n                                                _dnsServer?.WriteLog(ex);\n                                                return;\n                                            }\n                                            break;\n\n                                        default:\n                                            _dnsServer?.WriteLog($\"Failed to connect to the database server ({ex2.Number}). Please check the app config and make sure the database server is online.\");\n                                            _dnsServer?.WriteLog(ex);\n                                            return;\n                                    }\n                                }\n                            }\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsServer?.WriteLog(ex);\n                        }\n                    });\n                }\n                else\n                {\n                    //init via API call\n                    await ApplyConfig();\n                }\n            }\n            finally\n            {\n                _isStartupInit = false; //reset flag\n            }\n        }\n\n        public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (_enableLogging)\n                _channelWriter?.TryWrite(new LogEntry(timestamp, request, remoteEP, protocol, response));\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsLogPage> 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)\n        {\n            if (pageNumber == 0)\n                pageNumber = 1;\n\n            if (qname is not null)\n                qname = qname.ToLowerInvariant();\n\n            string whereClause = $\"server = '{_dnsServer?.ServerDomain}' AND \";\n\n            if (start is not null)\n                whereClause += \"timestamp >= @start AND \";\n\n            if (end is not null)\n                whereClause += \"timestamp <= @end AND \";\n\n            if (clientIpAddress is not null)\n                whereClause += \"client_ip = @client_ip AND \";\n\n            if (protocol is not null)\n                whereClause += \"protocol = @protocol AND \";\n\n            if (responseType is not null)\n                whereClause += \"response_type = @response_type AND \";\n\n            if (rcode is not null)\n                whereClause += \"rcode = @rcode AND \";\n\n            if (qname is not null)\n            {\n                if (qname.Contains('*'))\n                {\n                    whereClause += \"qname like @qname AND \";\n                    qname = qname.Replace(\"*\", \"%\");\n                }\n                else\n                {\n                    whereClause += \"qname = @qname AND \";\n                }\n            }\n\n            if (qtype is not null)\n                whereClause += \"qtype = @qtype AND \";\n\n            if (qclass is not null)\n                whereClause += \"qclass = @qclass AND \";\n\n            if (!string.IsNullOrEmpty(whereClause))\n                whereClause = whereClause.Substring(0, whereClause.Length - 5);\n\n            await using (SqlConnection connection = new SqlConnection(_connectionString + $\" Initial Catalog={_databaseName};\"))\n            {\n                await connection.OpenAsync();\n\n                //find total entries\n                long totalEntries;\n\n                await using (SqlCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"SELECT Count(*) FROM dns_logs\" + (string.IsNullOrEmpty(whereClause) ? \";\" : \" WHERE \" + whereClause + \";\");\n\n                    if (start is not null)\n                        command.Parameters.AddWithValue(\"@start\", start);\n\n                    if (end is not null)\n                        command.Parameters.AddWithValue(\"@end\", end);\n\n                    if (clientIpAddress is not null)\n                        command.Parameters.AddWithValue(\"@client_ip\", clientIpAddress.ToString());\n\n                    if (protocol is not null)\n                        command.Parameters.AddWithValue(\"@protocol\", (byte)protocol);\n\n                    if (responseType is not null)\n                        command.Parameters.AddWithValue(\"@response_type\", (byte)responseType);\n\n                    if (rcode is not null)\n                        command.Parameters.AddWithValue(\"@rcode\", (byte)rcode);\n\n                    if (qname is not null)\n                        command.Parameters.AddWithValue(\"@qname\", qname);\n\n                    if (qtype is not null)\n                        command.Parameters.AddWithValue(\"@qtype\", (short)qtype);\n\n                    if (qclass is not null)\n                        command.Parameters.AddWithValue(\"@qclass\", (ushort)qclass);\n\n                    totalEntries = Convert.ToInt64(await command.ExecuteScalarAsync() ?? 0L);\n                }\n\n                long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0);\n\n                if ((pageNumber > totalPages) || (pageNumber < 0))\n                    pageNumber = totalPages;\n\n                long endRowNum;\n                long startRowNum;\n\n                if (descendingOrder)\n                {\n                    endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage);\n                    startRowNum = endRowNum - entriesPerPage;\n                }\n                else\n                {\n                    endRowNum = pageNumber * entriesPerPage;\n                    startRowNum = endRowNum - entriesPerPage;\n                }\n\n                List<DnsLogEntry> entries = new List<DnsLogEntry>(entriesPerPage);\n\n                await using (SqlCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = @\"\nSELECT * FROM (\n    SELECT\n        ROW_NUMBER() OVER ( \n            ORDER BY dlid\n        ) row_num,\n        timestamp,\n        client_ip,\n        protocol,\n        response_type,\n        response_rtt,\n        rcode,\n        qname,\n        qtype,\n        qclass,\n        answer\n    FROM\n        dns_logs\n\" + (string.IsNullOrEmpty(whereClause) ? \"\" : \"WHERE \" + whereClause) + @\"\n) t\nWHERE \n    row_num > @start_row_num AND row_num <= @end_row_num\nORDER BY row_num\" + (descendingOrder ? \" DESC\" : \"\");\n\n                    command.Parameters.AddWithValue(\"@start_row_num\", startRowNum);\n                    command.Parameters.AddWithValue(\"@end_row_num\", endRowNum);\n\n                    if (start is not null)\n                        command.Parameters.AddWithValue(\"@start\", start);\n\n                    if (end is not null)\n                        command.Parameters.AddWithValue(\"@end\", end);\n\n                    if (clientIpAddress is not null)\n                        command.Parameters.AddWithValue(\"@client_ip\", clientIpAddress.ToString());\n\n                    if (protocol is not null)\n                        command.Parameters.AddWithValue(\"@protocol\", (byte)protocol);\n\n                    if (responseType is not null)\n                        command.Parameters.AddWithValue(\"@response_type\", (byte)responseType);\n\n                    if (rcode is not null)\n                        command.Parameters.AddWithValue(\"@rcode\", (byte)rcode);\n\n                    if (qname is not null)\n                        command.Parameters.AddWithValue(\"@qname\", qname);\n\n                    if (qtype is not null)\n                        command.Parameters.AddWithValue(\"@qtype\", (short)qtype);\n\n                    if (qclass is not null)\n                        command.Parameters.AddWithValue(\"@qclass\", (ushort)qclass);\n\n                    await using (SqlDataReader reader = await command.ExecuteReaderAsync())\n                    {\n                        while (await reader.ReadAsync())\n                        {\n                            double? responseRtt;\n\n                            if (reader.IsDBNull(5))\n                                responseRtt = null;\n                            else\n                                responseRtt = reader.GetFloat(5);\n\n                            DnsQuestionRecord? question;\n\n                            if (reader.IsDBNull(7))\n                                question = null;\n                            else\n                                question = new DnsQuestionRecord(reader.GetString(7), (DnsResourceRecordType)reader.GetInt16(8), (DnsClass)reader.GetInt16(9), false);\n\n                            string? answer;\n\n                            if (reader.IsDBNull(10))\n                                answer = null;\n                            else\n                                answer = reader.GetString(10);\n\n                            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));\n                        }\n                    }\n                }\n\n                return new DnsLogPage(pageNumber, totalPages, totalEntries, entries);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        #endregion\n\n        readonly struct LogEntry\n        {\n            #region variables\n\n            public readonly DateTime Timestamp;\n            public readonly DnsDatagram Request;\n            public readonly IPEndPoint RemoteEP;\n            public readonly DnsTransportProtocol Protocol;\n            public readonly DnsDatagram Response;\n\n            #endregion\n\n            #region constructor\n\n            public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n            {\n                Timestamp = timestamp;\n                Request = request;\n                RemoteEP = remoteEP;\n                Protocol = protocol;\n                Response = response;\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/QueryLogsSqlServerApp/QueryLogsSqlServerApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    <Version>2.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>QueryLogsSqlServerApp</AssemblyName>\n    <RootNamespace>QueryLogsSqlServer</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <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.\\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.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Data.SqlClient\" Version=\"6.1.2\" />\n    <PackageReference Include=\"System.Configuration.ConfigurationManager\" Version=\"9.0.10\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/QueryLogsSqlServerApp/dnsApp.config",
    "content": "{\n  \"enableLogging\": false,\n  \"maxQueueSize\": 1000000,\n  \"maxLogDays\": 0,\n  \"maxLogRecords\": 0,\n  \"databaseName\": \"DnsQueryLogs\",\n  \"connectionString\": \"Data Source=tcp:192.168.10.101,1433; User ID=username; Password=password; TrustServerCertificate=true;\"\n}\n"
  },
  {
    "path": "Apps/QueryLogsSqliteApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing Microsoft.Data.Sqlite;\nusing System;\nusing System.Collections.Generic;\nusing System.Data.Common;\nusing System.IO;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Channels;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace QueryLogsSqlite\n{\n    public sealed class App : IDnsApplication, IDnsQueryLogger, IDnsQueryLogs\n    {\n        #region variables\n\n        IDnsServer? _dnsServer;\n\n        bool _enableLogging;\n        int _maxQueueSize;\n        int _maxLogDays;\n        int _maxLogRecords;\n        bool _enableVacuum;\n        bool _useInMemoryDb;\n        string? _connectionString;\n\n        SqliteConnection? _inMemoryConnection;\n\n        Channel<LogEntry>? _channel;\n        ChannelWriter<LogEntry>? _channelWriter;\n        Thread? _consumerThread;\n        const int BULK_INSERT_COUNT = 1000;\n        const int BULK_INSERT_ERROR_DELAY = 10000;\n\n        readonly Timer _cleanupTimer;\n        const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000;\n        const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000;\n\n        #endregion\n\n        #region constructor\n\n        public App()\n        {\n            _cleanupTimer = new Timer(async delegate (object? state)\n            {\n                try\n                {\n                    await using (SqliteConnection connection = new SqliteConnection(_connectionString))\n                    {\n                        await connection.OpenAsync();\n\n                        int deletedRecords = 0;\n\n                        if (_maxLogRecords > 0)\n                        {\n                            await using (SqliteCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"DELETE FROM dns_logs WHERE ROWID IN (SELECT ROWID FROM dns_logs ORDER BY ROWID DESC LIMIT -1 OFFSET @maxLogRecords);\";\n\n                                command.Parameters.AddWithValue(\"@maxLogRecords\", _maxLogRecords);\n\n                                deletedRecords += await command.ExecuteNonQueryAsync();\n                            }\n                        }\n\n                        if (_maxLogDays > 0)\n                        {\n                            await using (SqliteCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"DELETE FROM dns_logs WHERE timestamp < @timestamp;\";\n\n                                command.Parameters.AddWithValue(\"@timestamp\", DateTime.UtcNow.AddDays(_maxLogDays * -1));\n\n                                deletedRecords += await command.ExecuteNonQueryAsync();\n                            }\n                        }\n\n                        if (_enableVacuum && (deletedRecords > 0))\n                        {\n                            await using (SqliteCommand command = connection.CreateCommand())\n                            {\n                                command.CommandText = \"VACUUM;\";\n\n                                await command.ExecuteNonQueryAsync();\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer?.WriteLog(ex);\n                }\n                finally\n                {\n                    try\n                    {\n                        _cleanupTimer?.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);\n                    }\n                    catch (ObjectDisposedException)\n                    { }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _enableLogging = false; //turn off logging\n\n            _cleanupTimer?.Dispose();\n\n            StopChannel();\n\n            if (_inMemoryConnection is not null)\n            {\n                _inMemoryConnection.Dispose();\n                _inMemoryConnection = null;\n            }\n\n            SqliteConnection.ClearAllPools(); //close db file\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private void StartNewChannel(int maxQueueSize)\n        {\n            ChannelWriter<LogEntry>? existingChannelWriter = _channelWriter;\n\n            //start new channel and consumer thread\n            BoundedChannelOptions options = new BoundedChannelOptions(maxQueueSize);\n            options.SingleWriter = true;\n            options.SingleReader = true;\n            options.FullMode = BoundedChannelFullMode.DropWrite;\n\n            _channel = Channel.CreateBounded<LogEntry>(options);\n            _channelWriter = _channel.Writer;\n            ChannelReader<LogEntry> channelReader = _channel.Reader;\n\n            _consumerThread = new Thread(async delegate ()\n            {\n                try\n                {\n                    List<LogEntry> logs = new List<LogEntry>(BULK_INSERT_COUNT);\n\n                    while (!_disposed && await channelReader.WaitToReadAsync())\n                    {\n                        while (!_disposed && (logs.Count < BULK_INSERT_COUNT) && channelReader.TryRead(out LogEntry log))\n                        {\n                            logs.Add(log);\n                        }\n\n                        if (logs.Count < 1)\n                            continue;\n\n                        await BulkInsertLogsAsync(logs);\n\n                        logs.Clear();\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer?.WriteLog(ex);\n                }\n            });\n\n            _consumerThread.Name = GetType().Name;\n            _consumerThread.IsBackground = true;\n            _consumerThread.Start();\n\n            //complete old channel to stop its consumer thread\n            existingChannelWriter?.TryComplete();\n        }\n\n        private void StopChannel()\n        {\n            _channel?.Writer.TryComplete();\n        }\n\n        private async Task BulkInsertLogsAsync(List<LogEntry> logs)\n        {\n            try\n            {\n                await using (SqliteConnection connection = new SqliteConnection(_connectionString))\n                {\n                    await connection.OpenAsync();\n\n                    await using (DbTransaction transaction = await connection.BeginTransactionAsync())\n                    {\n                        await using (SqliteCommand command = connection.CreateCommand())\n                        {\n                            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);\";\n\n                            SqliteParameter paramTimestamp = command.Parameters.Add(\"@timestamp\", SqliteType.Text);\n                            SqliteParameter paramClientIp = command.Parameters.Add(\"@client_ip\", SqliteType.Text);\n                            SqliteParameter paramProtocol = command.Parameters.Add(\"@protocol\", SqliteType.Integer);\n                            SqliteParameter paramResponseType = command.Parameters.Add(\"@response_type\", SqliteType.Integer);\n                            SqliteParameter paramResponseRtt = command.Parameters.Add(\"@response_rtt\", SqliteType.Real);\n                            SqliteParameter paramRcode = command.Parameters.Add(\"@rcode\", SqliteType.Integer);\n                            SqliteParameter paramQname = command.Parameters.Add(\"@qname\", SqliteType.Text);\n                            SqliteParameter paramQtype = command.Parameters.Add(\"@qtype\", SqliteType.Integer);\n                            SqliteParameter paramQclass = command.Parameters.Add(\"@qclass\", SqliteType.Integer);\n                            SqliteParameter paramAnswer = command.Parameters.Add(\"@answer\", SqliteType.Text);\n\n                            foreach (LogEntry log in logs)\n                            {\n                                paramTimestamp.Value = log.Timestamp.ToString(\"yyyy-MM-dd HH:mm:ss.FFFFFFF\");\n                                paramClientIp.Value = log.RemoteEP.Address.ToString();\n                                paramProtocol.Value = (int)log.Protocol;\n\n                                DnsServerResponseType responseType;\n\n                                if (log.Response.Tag == null)\n                                    responseType = DnsServerResponseType.Recursive;\n                                else\n                                    responseType = (DnsServerResponseType)log.Response.Tag;\n\n                                paramResponseType.Value = (int)responseType;\n\n                                if ((responseType == DnsServerResponseType.Recursive) && (log.Response.Metadata is not null))\n                                    paramResponseRtt.Value = log.Response.Metadata.RoundTripTime;\n                                else\n                                    paramResponseRtt.Value = DBNull.Value;\n\n                                paramRcode.Value = (int)log.Response.RCODE;\n\n                                if (log.Request.Question.Count > 0)\n                                {\n                                    DnsQuestionRecord query = log.Request.Question[0];\n\n                                    paramQname.Value = query.Name.ToLowerInvariant();\n                                    paramQtype.Value = (int)query.Type;\n                                    paramQclass.Value = (int)query.Class;\n                                }\n                                else\n                                {\n                                    paramQname.Value = DBNull.Value;\n                                    paramQtype.Value = DBNull.Value;\n                                    paramQclass.Value = DBNull.Value;\n                                }\n\n                                if (log.Response.Answer.Count == 0)\n                                {\n                                    paramAnswer.Value = DBNull.Value;\n                                }\n                                else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer)\n                                {\n                                    paramAnswer.Value = \"[ZONE TRANSFER]\";\n                                }\n                                else\n                                {\n                                    string? answer = null;\n\n                                    foreach (DnsResourceRecord record in log.Response.Answer)\n                                    {\n                                        if (answer is null)\n                                            answer = record.Type.ToString() + \" \" + record.RDATA.ToString();\n                                        else\n                                            answer += \", \" + record.Type.ToString() + \" \" + record.RDATA.ToString();\n                                    }\n\n                                    paramAnswer.Value = answer;\n                                }\n\n                                await command.ExecuteNonQueryAsync();\n                            }\n\n                            await transaction.CommitAsync();\n                        }\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer?.WriteLog(ex);\n\n                await Task.Delay(BULK_INSERT_ERROR_DELAY);\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            bool enableLogging = jsonConfig.GetPropertyValue(\"enableLogging\", true);\n            int maxQueueSize = jsonConfig.GetPropertyValue(\"maxQueueSize\", 200000);\n            _maxLogDays = jsonConfig.GetPropertyValue(\"maxLogDays\", 0);\n            _maxLogRecords = jsonConfig.GetPropertyValue(\"maxLogRecords\", 0);\n            _enableVacuum = jsonConfig.GetPropertyValue(\"enableVacuum\", false);\n            _useInMemoryDb = jsonConfig.GetPropertyValue(\"useInMemoryDb\", false);\n\n            if (_useInMemoryDb)\n            {\n                if (_inMemoryConnection is null)\n                {\n                    SqliteConnection.ClearAllPools(); //close db file, if any\n\n                    _connectionString = \"Data Source=QueryLogs;Mode=Memory;Cache=Shared\";\n\n                    _inMemoryConnection = new SqliteConnection(_connectionString);\n                    await _inMemoryConnection.OpenAsync();\n                }\n            }\n            else\n            {\n                if (_inMemoryConnection is not null)\n                {\n                    await _inMemoryConnection.DisposeAsync();\n                    _inMemoryConnection = null;\n                }\n\n                string sqliteDbPath = jsonConfig.GetPropertyValue(\"sqliteDbPath\", \"querylogs.db\");\n                string connectionString = jsonConfig.GetPropertyValue(\"connectionString\", \"Data Source='{sqliteDbPath}'; Cache=Shared;\");\n\n                if (!Path.IsPathRooted(sqliteDbPath))\n                    sqliteDbPath = Path.Combine(_dnsServer.ApplicationFolder, sqliteDbPath);\n\n                connectionString = connectionString.Replace(\"{sqliteDbPath}\", sqliteDbPath);\n\n                if ((_connectionString is not null) && !_connectionString.Equals(connectionString))\n                    SqliteConnection.ClearAllPools(); //close previous db file\n\n                _connectionString = connectionString;\n            }\n\n            await using (SqliteConnection connection = new SqliteConnection(_connectionString))\n            {\n                await connection.OpenAsync();\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = @\"\nCREATE TABLE IF NOT EXISTS dns_logs\n(\n    dlid INTEGER PRIMARY KEY,\n    timestamp DATETIME NOT NULL,\n    client_ip VARCHAR(39) NOT NULL,\n    protocol TINYINT NOT NULL,\n    response_type TINYINT NOT NULL,\n    response_rtt REAL,\n    rcode TINYINT NOT NULL,\n    qname VARCHAR(255),\n    qtype SMALLINT,\n    qclass SMALLINT,\n    answer TEXT\n);\n\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                try\n                {\n                    await using (SqliteCommand command = connection.CreateCommand())\n                    {\n                        command.CommandText = \"ALTER TABLE dns_logs ADD COLUMN response_rtt REAL;\";\n                        await command.ExecuteNonQueryAsync();\n                    }\n                }\n                catch\n                { }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_timestamp ON dns_logs (timestamp);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_client_ip ON dns_logs (client_ip);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_protocol ON dns_logs (protocol);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_response_type ON dns_logs (response_type);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_rcode ON dns_logs (rcode);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_qname ON dns_logs (qname);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_qtype ON dns_logs (qtype);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_qclass ON dns_logs (qclass);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_timestamp_client_ip ON dns_logs (timestamp, client_ip);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_timestamp_qname ON dns_logs (timestamp, qname);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_client_qname ON dns_logs (client_ip, qname);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_query ON dns_logs (qname, qtype);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"CREATE INDEX IF NOT EXISTS index_all ON dns_logs (timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass);\";\n                    await command.ExecuteNonQueryAsync();\n                }\n            }\n\n            if (enableLogging)\n            {\n                if (!_enableLogging || (_maxQueueSize != maxQueueSize))\n                    StartNewChannel(maxQueueSize);\n            }\n            else\n            {\n                StopChannel();\n            }\n\n            _enableLogging = enableLogging;\n            _maxQueueSize = maxQueueSize;\n\n            if ((_maxLogDays > 0) || (_maxLogRecords > 0))\n                _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            else\n                _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);\n\n            if (!jsonConfig.TryGetProperty(\"maxLogRecords\", out _))\n            {\n                config = config.Replace(\"\\\"sqliteDbPath\\\"\", \"\\\"maxLogRecords\\\": 0,\\r\\n  \\\"useInMemoryDb\\\": false,\\r\\n  \\\"sqliteDbPath\\\"\");\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n\n            if (!jsonConfig.TryGetProperty(\"enableVacuum\", out _))\n            {\n                config = config.Replace(\"\\\"useInMemoryDb\\\"\", \"\\\"enableVacuum\\\": false,\\r\\n  \\\"useInMemoryDb\\\"\");\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n\n            if (!jsonConfig.TryGetProperty(\"maxQueueSize\", out _))\n            {\n                config = config.Replace(\"\\\"maxLogDays\\\"\", \"\\\"maxQueueSize\\\": 200000,\\r\\n  \\\"maxLogDays\\\"\");\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n        }\n\n        public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (_enableLogging)\n                _channelWriter?.TryWrite(new LogEntry(timestamp, request, remoteEP, protocol, response));\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsLogPage> 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)\n        {\n            if (pageNumber == 0)\n                pageNumber = 1;\n\n            if (qname is not null)\n                qname = qname.ToLowerInvariant();\n\n            string whereClause = string.Empty;\n\n            if (start is not null)\n                whereClause += \"timestamp >= @start AND \";\n\n            if (end is not null)\n                whereClause += \"timestamp <= @end AND \";\n\n            if (clientIpAddress is not null)\n                whereClause += \"client_ip = @client_ip AND \";\n\n            if (protocol is not null)\n                whereClause += \"protocol = @protocol AND \";\n\n            if (responseType is not null)\n                whereClause += \"response_type = @response_type AND \";\n\n            if (rcode is not null)\n                whereClause += \"rcode = @rcode AND \";\n\n            if (qname is not null)\n            {\n                if (qname.Contains('*'))\n                {\n                    whereClause += \"qname like @qname AND \";\n                    qname = qname.Replace(\"*\", \"%\");\n                }\n                else\n                {\n                    whereClause += \"qname = @qname AND \";\n                }\n            }\n\n            if (qtype is not null)\n                whereClause += \"qtype = @qtype AND \";\n\n            if (qclass is not null)\n                whereClause += \"qclass = @qclass AND \";\n\n            if (!string.IsNullOrEmpty(whereClause))\n                whereClause = whereClause.Substring(0, whereClause.Length - 5);\n\n            await using (SqliteConnection connection = new SqliteConnection(_connectionString))\n            {\n                await connection.OpenAsync();\n\n                //find total entries\n                long totalEntries;\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = \"SELECT Count(*) FROM dns_logs\" + (string.IsNullOrEmpty(whereClause) ? \";\" : \" WHERE \" + whereClause + \";\");\n\n                    if (start is not null)\n                        command.Parameters.AddWithValue(\"@start\", start);\n\n                    if (end is not null)\n                        command.Parameters.AddWithValue(\"@end\", end);\n\n                    if (clientIpAddress is not null)\n                        command.Parameters.AddWithValue(\"@client_ip\", clientIpAddress.ToString());\n\n                    if (protocol is not null)\n                        command.Parameters.AddWithValue(\"@protocol\", (byte)protocol);\n\n                    if (responseType is not null)\n                        command.Parameters.AddWithValue(\"@response_type\", (byte)responseType);\n\n                    if (rcode is not null)\n                        command.Parameters.AddWithValue(\"@rcode\", (byte)rcode);\n\n                    if (qname is not null)\n                        command.Parameters.AddWithValue(\"@qname\", qname);\n\n                    if (qtype is not null)\n                        command.Parameters.AddWithValue(\"@qtype\", (ushort)qtype);\n\n                    if (qclass is not null)\n                        command.Parameters.AddWithValue(\"@qclass\", (ushort)qclass);\n\n                    totalEntries = Convert.ToInt64(await command.ExecuteScalarAsync());\n                }\n\n                long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0);\n\n                if ((pageNumber > totalPages) || (pageNumber < 0))\n                    pageNumber = totalPages;\n\n                long endRowNum;\n                long startRowNum;\n\n                if (descendingOrder)\n                {\n                    endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage);\n                    startRowNum = endRowNum - entriesPerPage;\n                }\n                else\n                {\n                    endRowNum = pageNumber * entriesPerPage;\n                    startRowNum = endRowNum - entriesPerPage;\n                }\n\n                List<DnsLogEntry> entries = new List<DnsLogEntry>(entriesPerPage);\n\n                await using (SqliteCommand command = connection.CreateCommand())\n                {\n                    command.CommandText = @\"\nSELECT * FROM (\n    SELECT\n        ROW_NUMBER() OVER ( \n            ORDER BY dlid\n        ) row_num,\n        timestamp,\n        client_ip,\n        protocol,\n        response_type,\n        response_rtt,\n        rcode,\n        qname,\n        qtype,\n        qclass,\n        answer\n    FROM\n        dns_logs\n\" + (string.IsNullOrEmpty(whereClause) ? \"\" : \"WHERE \" + whereClause) + @\"\n) t\nWHERE \n    row_num > @start_row_num AND row_num <= @end_row_num\nORDER BY row_num\" + (descendingOrder ? \" DESC\" : \"\");\n\n                    command.Parameters.AddWithValue(\"@start_row_num\", startRowNum);\n                    command.Parameters.AddWithValue(\"@end_row_num\", endRowNum);\n\n                    if (start is not null)\n                        command.Parameters.AddWithValue(\"@start\", start);\n\n                    if (end is not null)\n                        command.Parameters.AddWithValue(\"@end\", end);\n\n                    if (clientIpAddress is not null)\n                        command.Parameters.AddWithValue(\"@client_ip\", clientIpAddress.ToString());\n\n                    if (protocol is not null)\n                        command.Parameters.AddWithValue(\"@protocol\", (byte)protocol);\n\n                    if (responseType is not null)\n                        command.Parameters.AddWithValue(\"@response_type\", (byte)responseType);\n\n                    if (rcode is not null)\n                        command.Parameters.AddWithValue(\"@rcode\", (byte)rcode);\n\n                    if (qname is not null)\n                        command.Parameters.AddWithValue(\"@qname\", qname);\n\n                    if (qtype is not null)\n                        command.Parameters.AddWithValue(\"@qtype\", (ushort)qtype);\n\n                    if (qclass is not null)\n                        command.Parameters.AddWithValue(\"@qclass\", (ushort)qclass);\n\n                    await using (SqliteDataReader reader = await command.ExecuteReaderAsync())\n                    {\n                        while (await reader.ReadAsync())\n                        {\n                            double? responseRtt;\n\n                            if (reader.IsDBNull(5))\n                                responseRtt = null;\n                            else\n                                responseRtt = reader.GetDouble(5);\n\n                            DnsQuestionRecord? question;\n\n                            if (reader.IsDBNull(7))\n                                question = null;\n                            else\n                                question = new DnsQuestionRecord(reader.GetString(7), (DnsResourceRecordType)reader.GetInt32(8), (DnsClass)reader.GetInt32(9), false);\n\n                            string? answer;\n\n                            if (reader.IsDBNull(10))\n                                answer = null;\n                            else\n                                answer = reader.GetString(10);\n\n                            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));\n                        }\n                    }\n                }\n\n                return new DnsLogPage(pageNumber, totalPages, totalEntries, entries);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Logs all incoming DNS requests and their responses in a Sqlite database that can be queried from the DNS Server web console.\"; } }\n\n        #endregion\n\n        readonly struct LogEntry\n        {\n            #region variables\n\n            public readonly DateTime Timestamp;\n            public readonly DnsDatagram Request;\n            public readonly IPEndPoint RemoteEP;\n            public readonly DnsTransportProtocol Protocol;\n            public readonly DnsDatagram Response;\n\n            #endregion\n\n            #region constructor\n\n            public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n            {\n                Timestamp = timestamp;\n                Request = request;\n                RemoteEP = remoteEP;\n                Protocol = protocol;\n                Response = response;\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/QueryLogsSqliteApp/QueryLogsSqliteApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<Version>8.0</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>QueryLogsSqliteApp</AssemblyName>\n\t\t<RootNamespace>QueryLogsSqlite</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<Description>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.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t\t<Nullable>enable</Nullable>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Microsoft.Data.Sqlite\" Version=\"9.0.10\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/QueryLogsSqliteApp/dnsApp.config",
    "content": "{\n  \"enableLogging\": true,\n  \"maxQueueSize\": 200000,\n  \"maxLogDays\": 7,\n  \"maxLogRecords\": 10000,\n  \"enableVacuum\": false,\n  \"useInMemoryDb\": false,\n  \"sqliteDbPath\": \"querylogs.db\",\n  \"connectionString\": \"Data Source='{sqliteDbPath}'; Cache=Shared;\"\n}\n"
  },
  {
    "path": "Apps/SplitHorizonApp/AddressTranslation.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace SplitHorizon\n{\n    public sealed class AddressTranslation : IDnsApplication, IDnsPostProcessor, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference\n    {\n        #region variables\n\n        byte _appPreference;\n\n        bool _enableAddressTranslation;\n        Dictionary<string, string> _domainGroupMap;\n        Dictionary<NetworkAddress, string> _networkGroupMap;\n        Dictionary<string, Group> _groups;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            if (string.IsNullOrEmpty(config) || config.StartsWith('#'))\n            {\n                //replace old config with default config\n                config = \"\"\"\n{\n    \"networks\": {\n        \"custom-networks\": [\n            \"172.16.1.0/24\",\n            \"172.16.10.0/24\",\n            \"172.16.2.1\"\n        ]\n    },\n    \"enableAddressTranslation\": false,\n    \"domainGroupMap\": {\n\t    \"example.com\": \"local1\"\n\t},\n    \"networkGroupMap\": {\n        \"10.0.0.0/8\": \"local1\",\n        \"172.16.0.0/12\": \"local2\",\n        \"192.168.0.0/16\": \"local3\"\n    },\n    \"groups\": [\n        {\n            \"name\": \"local1\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.0/24\": \"10.0.0.0/24\",\n               \"5.6.7.8\": \"10.0.0.5\"\n            }\n        },\n        {\n            \"name\": \"local2\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"172.16.0.4\",\n               \"5.6.7.8\": \"172.16.0.5\"\n            }\n        },\n        {\n            \"name\": \"local3\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"192.168.0.4\",\n               \"5.6.7.8\": \"192.168.0.5\"\n            }\n        }\n    ]\n}\n\"\"\";\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n\n            do\n            {\n                using JsonDocument jsonDocument = JsonDocument.Parse(config);\n                JsonElement jsonConfig = jsonDocument.RootElement;\n\n                _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue(\"appPreference\", 40));\n\n                if (!jsonConfig.TryGetProperty(\"enableAddressTranslation\", out _))\n                {\n                    //update old config with default config\n                    config = config.TrimEnd(' ', '\\t', '\\r', '\\n');\n                    config = config.Substring(0, config.Length - 1);\n                    config = config.TrimEnd(' ', '\\t', '\\r', '\\n');\n                    config += \"\"\"\n,\n    \"enableAddressTranslation\": false,\n    \"domainGroupMap\": {\n\t    \"example.com\": \"local1\"\n\t},\n    \"networkGroupMap\": {\n        \"10.0.0.0/8\": \"local1\",\n        \"172.16.0.0/12\": \"local2\",\n        \"192.168.0.0/16\": \"local3\"\n    },\n    \"groups\": [\n        {\n            \"name\": \"local1\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.0/24\": \"10.0.0.0/24\",\n               \"5.6.7.8\": \"10.0.0.5\"\n            }\n        },\n        {\n            \"name\": \"local2\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"172.16.0.4\",\n               \"5.6.7.8\": \"172.16.0.5\"\n            }\n        },\n        {\n            \"name\": \"local3\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"192.168.0.4\",\n               \"5.6.7.8\": \"192.168.0.5\"\n            }\n        }\n    ]\n}\n\"\"\";\n                    await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n\n                    //reparse config\n                    continue;\n                }\n\n                _enableAddressTranslation = jsonConfig.GetProperty(\"enableAddressTranslation\").GetBoolean();\n\n                if (!jsonConfig.TryReadObjectAsMap(\"domainGroupMap\", delegate (string domain, JsonElement jsonGroupName)\n                    {\n                        return new Tuple<string, string>(domain, jsonGroupName.GetString());\n                    }, out _domainGroupMap))\n                {\n                    _domainGroupMap = new Dictionary<string, string>(1)\n                    {\n                        { \"example.com\", \"local1\" }\n                    };\n\n                    config = config.Replace(\"\\\"networkGroupMap\\\": \", \"\\\"domainGroupMap\\\": {\\r\\n        \\\"example.com\\\": \\\"local1\\\"\\r\\n    },\\r\\n    \\\"networkGroupMap\\\": \");\n                    await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n                }\n\n                _networkGroupMap = jsonConfig.ReadObjectAsMap(\"networkGroupMap\", delegate (string strNetworkAddress, JsonElement jsonGroupName)\n                {\n                    if (!NetworkAddress.TryParse(strNetworkAddress, out NetworkAddress networkAddress))\n                        throw new InvalidOperationException(\"Network group map contains an invalid network address: \" + strNetworkAddress);\n\n                    return new Tuple<NetworkAddress, string>(networkAddress, jsonGroupName.GetString());\n                });\n\n                _groups = jsonConfig.ReadArrayAsMap(\"groups\", delegate (JsonElement jsonGroup)\n                {\n                    Group group = new Group(jsonGroup);\n                    return new Tuple<string, Group>(group.Name, group);\n                });\n\n                break;\n            }\n            while (true);\n        }\n\n        public Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            if (!_enableAddressTranslation)\n                return Task.FromResult(response);\n\n            if (response.RCODE != DnsResponseCode.NoError)\n                return Task.FromResult(response);\n\n            DnsQuestionRecord question = request.Question[0];\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    break;\n\n                default:\n                    return Task.FromResult(response);\n            }\n\n            if (response.Answer.Count == 0)\n                return Task.FromResult(response);\n\n            string groupName = null;\n            string qname = question.Name;\n            string domain = null;\n\n            foreach (KeyValuePair<string, string> entry in _domainGroupMap)\n            {\n                if ((qname.Equals(entry.Key, StringComparison.OrdinalIgnoreCase) || qname.EndsWith(\".\" + entry.Key, StringComparison.OrdinalIgnoreCase))\n                    && ((domain is null) || (entry.Key.Length > domain.Length)))\n                {\n                    domain = entry.Key;\n                    groupName = entry.Value;\n                }\n            }\n\n            if (groupName is null)\n            {\n                IPAddress remoteIP = remoteEP.Address;\n                NetworkAddress network = null;\n\n                foreach (KeyValuePair<NetworkAddress, string> entry in _networkGroupMap)\n                {\n                    if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength)))\n                    {\n                        network = entry.Key;\n                        groupName = entry.Value;\n                    }\n                }\n            }\n\n            if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled)\n                return Task.FromResult(response);\n\n            List<DnsResourceRecord> newAnswer = new List<DnsResourceRecord>(response.Answer.Count);\n\n            foreach (DnsResourceRecord answer in response.Answer)\n            {\n                switch (answer.Type)\n                {\n                    case DnsResourceRecordType.A:\n                        {\n                            IPAddress externalIp = (answer.RDATA as DnsARecordData).Address;\n\n                            if (group.TryExternalToInternalTranslation(externalIp, out IPAddress internalIp))\n                                newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TTL, new DnsARecordData(internalIp)));\n                            else\n                                newAnswer.Add(answer);\n                        }\n                        break;\n\n                    case DnsResourceRecordType.AAAA:\n                        {\n                            IPAddress externalIp = (answer.RDATA as DnsAAAARecordData).Address;\n\n                            if (group.TryExternalToInternalTranslation(externalIp, out IPAddress internalIp))\n                                newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TTL, new DnsAAAARecordData(internalIp)));\n                            else\n                                newAnswer.Add(answer);\n                        }\n                        break;\n\n                    default:\n                        newAnswer.Add(answer);\n                        break;\n                }\n            }\n\n            return Task.FromResult(response.Clone(newAnswer));\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)\n        {\n            if (!_enableAddressTranslation)\n                return Task.FromResult<DnsDatagram>(null);\n\n            DnsQuestionRecord question = request.Question[0];\n            if (question.Type != DnsResourceRecordType.PTR)\n                return Task.FromResult<DnsDatagram>(null);\n\n            IPAddress remoteIP = remoteEP.Address;\n            NetworkAddress network = null;\n            string groupName = null;\n\n            foreach (KeyValuePair<NetworkAddress, string> entry in _networkGroupMap)\n            {\n                if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength)))\n                {\n                    network = entry.Key;\n                    groupName = entry.Value;\n                }\n            }\n\n            if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled || !group.TranslateReverseLookups)\n                return Task.FromResult<DnsDatagram>(null);\n\n            IPAddress ptrIpAddress = IPAddressExtensions.ParseReverseDomain(question.Name);\n\n            if (!group.TryInternalToExternalTranslation(ptrIpAddress, out IPAddress externalIp))\n                return Task.FromResult<DnsDatagram>(null);\n\n            IReadOnlyList<DnsResourceRecord> answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, 600, new DnsCNAMERecordData(externalIp.GetReverseDomain())) };\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public byte Preference\n        { get { return _appPreference; } }\n\n        #endregion\n\n        class Group\n        {\n            #region variables\n\n            readonly string _name;\n            readonly bool _enabled;\n            readonly bool _translateReverseLookups;\n            readonly Dictionary<IPAddress, IPAddress> _externalToInternalTranslation;\n            readonly Dictionary<IPAddress, IPAddress> _internalToExternalTranslation;\n            readonly List<KeyValuePair<NetworkAddress, NetworkAddress>> _externalToInternalNetworkTranslation;\n\n            #endregion\n\n            #region constructor\n\n            public Group(JsonElement jsonGroup)\n            {\n                _name = jsonGroup.GetProperty(\"name\").GetString();\n                _enabled = jsonGroup.GetProperty(\"enabled\").GetBoolean();\n                _translateReverseLookups = jsonGroup.GetProperty(\"translateReverseLookups\").GetBoolean();\n\n                JsonElement jsonExternalToInternalTranslation = jsonGroup.GetProperty(\"externalToInternalTranslation\");\n\n                Dictionary<IPAddress, IPAddress> externalToInternalIpTranslation = new Dictionary<IPAddress, IPAddress>();\n                Dictionary<IPAddress, IPAddress> internalToExternalIpTranslation = new Dictionary<IPAddress, IPAddress>();\n                List<KeyValuePair<NetworkAddress, NetworkAddress>> externalToInternalNetworkTranslation = new List<KeyValuePair<NetworkAddress, NetworkAddress>>();\n\n                foreach (JsonProperty jsonProperty in jsonExternalToInternalTranslation.EnumerateObject())\n                {\n                    string strExternal = jsonProperty.Name;\n                    string strInternal = jsonProperty.Value.GetString();\n\n                    NetworkAddress external = NetworkAddress.Parse(strExternal);\n                    NetworkAddress @internal = NetworkAddress.Parse(strInternal);\n\n                    if (external.AddressFamily != @internal.AddressFamily)\n                        throw new InvalidDataException(\"External to internal translation entries must have same address family: \" + strExternal + \" - \" + strInternal);\n\n                    if (external.PrefixLength != @internal.PrefixLength)\n                        throw new InvalidDataException(\"External to internal translation entries must have same prefix length: \" + strExternal + \" - \" + strInternal);\n\n                    if (\n                        ((external.AddressFamily == AddressFamily.InterNetwork) && (external.PrefixLength == 32)) ||\n                        ((external.AddressFamily == AddressFamily.InterNetworkV6) && (external.PrefixLength == 128))\n                       )\n                    {\n                        externalToInternalIpTranslation.TryAdd(external.Address, @internal.Address);\n\n                        if (_translateReverseLookups)\n                            internalToExternalIpTranslation.TryAdd(@internal.Address, external.Address);\n                    }\n                    else\n                    {\n                        externalToInternalNetworkTranslation.Add(new KeyValuePair<NetworkAddress, NetworkAddress>(external, @internal));\n                    }\n                }\n\n                _externalToInternalTranslation = externalToInternalIpTranslation;\n\n                if (_translateReverseLookups)\n                    _internalToExternalTranslation = internalToExternalIpTranslation;\n\n                _externalToInternalNetworkTranslation = externalToInternalNetworkTranslation;\n            }\n\n            #endregion\n\n            #region public\n\n            public bool TryExternalToInternalTranslation(IPAddress externalIp, out IPAddress internalIp)\n            {\n                if (_externalToInternalTranslation.TryGetValue(externalIp, out internalIp))\n                    return true;\n\n                foreach (KeyValuePair<NetworkAddress, NetworkAddress> networkEntry in _externalToInternalNetworkTranslation)\n                {\n                    NetworkAddress external = networkEntry.Key;\n\n                    if (external.AddressFamily != externalIp.AddressFamily)\n                        continue;\n\n                    if (external.Contains(externalIp))\n                    {\n                        NetworkAddress @internal = networkEntry.Value;\n\n                        switch (external.AddressFamily)\n                        {\n                            case AddressFamily.InterNetwork:\n                                {\n                                    uint hostMask = ~(0xFFFFFFFFu << (32 - external.PrefixLength));\n                                    uint host = externalIp.ConvertIpToNumber() & hostMask;\n                                    uint addr = @internal.Address.ConvertIpToNumber();\n                                    uint internalAddr = addr | host;\n\n                                    internalIp = IPAddressExtensions.ConvertNumberToIp(internalAddr);\n                                    return true;\n                                }\n\n                            case AddressFamily.InterNetworkV6:\n                                {\n                                    byte[] externalIpBytes = externalIp.GetAddressBytes();\n                                    byte[] internalIpBytes = @internal.Address.GetAddressBytes();\n                                    int copyBytes = external.PrefixLength / 8;\n                                    int balanceBits = external.PrefixLength - (copyBytes * 8);\n\n                                    Buffer.BlockCopy(externalIpBytes, copyBytes + 1, internalIpBytes, copyBytes + 1, 16 - copyBytes - 1);\n\n                                    if (balanceBits > 0)\n                                    {\n                                        int mask = 0xFF << (8 - balanceBits);\n                                        internalIpBytes[copyBytes] = (byte)((internalIpBytes[copyBytes] & mask) | (externalIpBytes[copyBytes] & ~mask));\n                                    }\n\n                                    internalIp = new IPAddress(internalIpBytes);\n                                    return true;\n                                }\n\n                            default:\n                                throw new InvalidOperationException();\n                        }\n                    }\n                }\n\n                internalIp = null;\n                return false;\n            }\n\n            public bool TryInternalToExternalTranslation(IPAddress internalIp, out IPAddress externalIp)\n            {\n                if (_internalToExternalTranslation.TryGetValue(internalIp, out externalIp))\n                    return true;\n\n                foreach (KeyValuePair<NetworkAddress, NetworkAddress> networkEntry in _externalToInternalNetworkTranslation)\n                {\n                    NetworkAddress @internal = networkEntry.Value;\n\n                    if (@internal.AddressFamily != internalIp.AddressFamily)\n                        continue;\n\n                    if (@internal.Contains(internalIp))\n                    {\n                        NetworkAddress external = networkEntry.Key;\n\n                        switch (@internal.AddressFamily)\n                        {\n                            case AddressFamily.InterNetwork:\n                                {\n                                    uint hostMask = ~(0xFFFFFFFFu << (32 - @internal.PrefixLength));\n                                    uint host = internalIp.ConvertIpToNumber() & hostMask;\n                                    uint addr = external.Address.ConvertIpToNumber();\n                                    uint externalAddr = addr | host;\n\n                                    externalIp = IPAddressExtensions.ConvertNumberToIp(externalAddr);\n                                    return true;\n                                }\n\n                            case AddressFamily.InterNetworkV6:\n                                {\n                                    byte[] internalIpBytes = internalIp.GetAddressBytes();\n                                    byte[] externalIpBytes = external.Address.GetAddressBytes();\n                                    int copyBytes = @internal.PrefixLength / 8;\n                                    int balanceBits = @internal.PrefixLength - (copyBytes * 8);\n\n                                    Buffer.BlockCopy(internalIpBytes, copyBytes + 1, externalIpBytes, copyBytes + 1, 16 - copyBytes - 1);\n\n                                    if (balanceBits > 0)\n                                    {\n                                        int mask = 0xFF << (8 - balanceBits);\n                                        externalIpBytes[copyBytes] = (byte)((externalIpBytes[copyBytes] & mask) | (internalIpBytes[copyBytes] & ~mask));\n                                    }\n\n                                    externalIp = new IPAddress(externalIpBytes);\n                                    return true;\n                                }\n\n                            default:\n                                throw new InvalidOperationException();\n                        }\n                    }\n                }\n\n                externalIp = null;\n                return false;\n            }\n\n            #endregion\n\n            #region properties\n\n            public string Name\n            { get { return _name; } }\n\n            public bool Enabled\n            { get { return _enabled; } }\n\n            public bool TranslateReverseLookups\n            { get { return _translateReverseLookups; } }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/SplitHorizonApp/README.md",
    "content": "# Split Horizon\n\nThe Split Horizon app provides two distinct features which can be used independently:\n\n1. 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.\n\n1. 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.\n\nThe following sections describe each feature in more detail.\n\n## A / AAAA / CNAME\n\nTo 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.\n\nEach `APP` record is configured with a JSON document which looks like the following:\n\n```\n{\n  \"public\": [\n    \"1.1.1.1\",\n    \"2.2.2.2\"\n  ],\n  \"private\": [\n    \"192.168.1.1\",\n    \"::1\"\n  ],\n  \"custom-networks\": [\n    \"172.16.1.1\"\n  ],\n  \"10.0.0.0/8\": [\n    \"10.1.1.1\"\n  ]\n}\n```\n\nAn example for `CNAME` replacements:\n\n```\n{\n  \"public\": \"api.example.com\",\n  \"private\": \"api.example.corp\",\n  \"custom-networks\": \"custom.example.corp\",\n  \"10.0.0.0/8\": \"api.intranet.example.corp\"\n}\n```\n\nKeys can be one of the following:\n\n- a network specification (like `10.0.0.0/8`)\n- a named network defined in the global app configuration (see [Address Translation])\n- `private`: private IP ranges defined in RFC 1918\n- `public`: all IPs outside the private IP ranges defined in RFC 1918\n\nValues 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.\n\nThe 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.\n\n## Address Translation\n\nTranslates 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.\n\n### Configuration\n\nThis 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:\n\n```\n{\n    \"networks\": {\n        \"custom-networks\": [\n            \"172.16.1.0/24\",\n            \"172.16.10.0/24\",\n            \"172.16.2.1\"\n        ]\n    },\n    \"enableAddressTranslation\": false,\n    \"networkGroupMap\": {\n        \"10.0.0.0/8\": \"local1\",\n        \"172.16.0.0/12\": \"local2\",\n        \"192.168.0.0/16\": \"local3\"\n    },\n    \"groups\": [\n        {\n            \"name\": \"local1\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.0/24\": \"10.0.0.0/24\",\n               \"5.6.7.8\": \"10.0.0.5\"\n            }\n        },\n        {\n            \"name\": \"local2\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"172.16.0.4\",\n               \"5.6.7.8\": \"172.16.0.5\"\n            }\n        },\n        {\n            \"name\": \"local3\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"192.168.0.4\",\n               \"5.6.7.8\": \"192.168.0.5\"\n            }\n        }\n    ]\n}\n```\n\nThe individual settings are:\n\n- `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.\n- `enableAddressTranslation`: when set to `false`, address translation is disabled and the original response is passed through unmodified.\n- `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`.\n- `groups`: a list of groups. A group has the following properties:\n  - `name`: the name of the group.\n  - `enabled`: flag whether to perform address translation for this group.\n  - `translateReverseLookups`: flag whether to respond to `PTR` queries for internal IPs (see below)\n  - `externalToInternalTranslation`: a mapping from external to internal network addresses (see below). The networks must be of the same size (have the same prefix length).\n\n### Processing\n\nForward lookups (`A` and `AAAA`) which fulfill all of the following requirements are processed by this app:\n\n- the requesting client is a member of a group defined in `networkGroupMap`\n- the response code is `NoError`\n- the response has at least one answer\n\nNote that `NXDOMAIN`, `SERVFAIL`, and `NODATA` answers are passed through unmodified.\n\nFor 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`.\n\nIf `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`.\n"
  },
  {
    "path": "Apps/SplitHorizonApp/SimpleAddress.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace SplitHorizon\n{\n    public sealed class SimpleAddress : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        static Dictionary<string, List<NetworkAddress>> _networks;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            if (string.IsNullOrEmpty(config) || config.StartsWith('#'))\n            {\n                //replace old config with default config\n                config = \"\"\"\n{\n    \"networks\": {\n        \"custom-networks\": [\n            \"172.16.1.0/24\",\n            \"172.16.10.0/24\",\n            \"172.16.2.1\"\n        ]\n    },\n    \"enableAddressTranslation\": false,\n    \"networkGroupMap\": {\n        \"10.0.0.0/8\": \"local1\",\n        \"172.16.0.0/12\": \"local2\",\n        \"192.168.0.0/16\": \"local3\"\n    },\n    \"groups\": [\n        {\n            \"name\": \"local1\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"10.0.0.4\",\n               \"5.6.7.8\": \"10.0.0.5\"\n            }\n        },\n        {\n            \"name\": \"local2\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"172.16.0.4\",\n               \"5.6.7.8\": \"172.16.0.5\"\n            }\n        },\n        {\n            \"name\": \"local3\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"192.168.0.4\",\n               \"5.6.7.8\": \"192.168.0.5\"\n            }\n        }\n    ]\n}\n\"\"\";\n\n                await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, \"dnsApp.config\"), config);\n            }\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            if (jsonConfig.TryGetProperty(\"networks\", out JsonElement jsonNetworks))\n            {\n                Dictionary<string, List<NetworkAddress>> networks = new Dictionary<string, List<NetworkAddress>>();\n\n                foreach (JsonProperty jsonProperty in jsonNetworks.EnumerateObject())\n                {\n                    string networkName = jsonProperty.Name;\n\n                    JsonElement jsonNetworkAddresses = jsonProperty.Value;\n                    if (jsonNetworkAddresses.ValueKind == JsonValueKind.Array)\n                    {\n                        List<NetworkAddress> networkAddresses = new List<NetworkAddress>(jsonNetworkAddresses.GetArrayLength());\n\n                        foreach (JsonElement jsonNetworkAddress in jsonNetworkAddresses.EnumerateArray())\n                            networkAddresses.Add(NetworkAddress.Parse(jsonNetworkAddress.GetString()));\n\n                        networks.TryAdd(networkName, networkAddresses);\n                    }\n                }\n\n                _networks = networks;\n            }\n            else\n            {\n                _networks = new Dictionary<string, List<NetworkAddress>>(1);\n            }\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))\n                    {\n                        JsonElement jsonAppRecordData = jsonDocument.RootElement;\n                        JsonElement jsonAddresses = default;\n\n                        NetworkAddress selectedNetwork = null;\n\n                        foreach (JsonProperty jsonProperty in jsonAppRecordData.EnumerateObject())\n                        {\n                            string name = jsonProperty.Name;\n\n                            if ((name == \"public\") || (name == \"private\"))\n                                continue;\n\n                            if (_networks.TryGetValue(name, out List<NetworkAddress> networkAddresses))\n                            {\n                                foreach (NetworkAddress networkAddress in networkAddresses)\n                                {\n                                    if (networkAddress.Contains(remoteEP.Address))\n                                    {\n                                        jsonAddresses = jsonProperty.Value;\n                                        break;\n                                    }\n                                }\n\n                                if (jsonAddresses.ValueKind != JsonValueKind.Undefined)\n                                    break;\n                            }\n                            else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress))\n                            {\n                                if (networkAddress.Contains(remoteEP.Address) && ((selectedNetwork is null) || (networkAddress.PrefixLength > selectedNetwork.PrefixLength)))\n                                {\n                                    selectedNetwork = networkAddress;\n                                    jsonAddresses = jsonProperty.Value;\n                                }\n                            }\n                        }\n\n                        if (jsonAddresses.ValueKind == JsonValueKind.Undefined)\n                        {\n                            if (NetUtilities.IsPrivateIP(remoteEP.Address))\n                            {\n                                if (!jsonAppRecordData.TryGetProperty(\"private\", out jsonAddresses))\n                                    return Task.FromResult<DnsDatagram>(null);\n                            }\n                            else\n                            {\n                                if (!jsonAppRecordData.TryGetProperty(\"public\", out jsonAddresses))\n                                    return Task.FromResult<DnsDatagram>(null);\n                            }\n                        }\n\n                        List<DnsResourceRecord> answers = new List<DnsResourceRecord>();\n\n                        switch (question.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                                foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())\n                                {\n                                    if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetwork))\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address)));\n                                }\n                                break;\n\n                            case DnsResourceRecordType.AAAA:\n                                foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())\n                                {\n                                    if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetworkV6))\n                                        answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)));\n                                }\n                                break;\n                        }\n\n                        if (answers.Count == 0)\n                            return Task.FromResult<DnsDatagram>(null);\n\n                        if (answers.Count > 1)\n                            answers.Shuffle();\n\n                        return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));\n                    }\n\n                default:\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        internal static Dictionary<string, List<NetworkAddress>> Networks\n        { get { return _networks; } }\n\n        public string Description\n        { get { return \"Returns A or AAAA records with different set of IP addresses for clients querying over public, private, or other specified networks.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"public\"\": [\n    \"\"1.1.1.1\"\",\n    \"\"2.2.2.2\"\"\n  ],\n  \"\"private\"\": [\n    \"\"192.168.1.1\"\",\n    \"\"::1\"\"\n  ],\n  \"\"custom-networks\"\": [\n    \"\"172.16.1.1\"\"\n  ],\n  \"\"10.0.0.0/8\"\": [\n    \"\"10.1.1.1\"\"\n  ]\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/SplitHorizonApp/SimpleCNAME.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace SplitHorizon\n{\n    public sealed class SimpleCNAME : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            //SimpleAddress loads the shared config\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData);\n            JsonElement jsonAppRecordData = jsonDocument.RootElement;\n            JsonElement jsonCname = default;\n\n            NetworkAddress selectedNetwork = null;\n\n            foreach (JsonProperty jsonProperty in jsonAppRecordData.EnumerateObject())\n            {\n                string name = jsonProperty.Name;\n\n                if ((name == \"public\") || (name == \"private\"))\n                    continue;\n\n                if (SimpleAddress.Networks.TryGetValue(name, out List<NetworkAddress> networkAddresses))\n                {\n                    foreach (NetworkAddress networkAddress in networkAddresses)\n                    {\n                        if (networkAddress.Contains(remoteEP.Address))\n                        {\n                            jsonCname = jsonProperty.Value;\n                            break;\n                        }\n                    }\n\n                    if (jsonCname.ValueKind != JsonValueKind.Undefined)\n                        break;\n                }\n                else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress))\n                {\n                    if (networkAddress.Contains(remoteEP.Address) && ((selectedNetwork is null) || (networkAddress.PrefixLength > selectedNetwork.PrefixLength)))\n                    {\n                        selectedNetwork = networkAddress;\n                        jsonCname = jsonProperty.Value;\n                    }\n                }\n            }\n\n            if (jsonCname.ValueKind == JsonValueKind.Undefined)\n            {\n                if (NetUtilities.IsPrivateIP(remoteEP.Address))\n                {\n                    if (!jsonAppRecordData.TryGetProperty(\"private\", out jsonCname))\n                        return Task.FromResult<DnsDatagram>(null);\n                }\n                else\n                {\n                    if (!jsonAppRecordData.TryGetProperty(\"public\", out jsonCname))\n                        return Task.FromResult<DnsDatagram>(null);\n                }\n            }\n\n            string cname = jsonCname.GetString();\n            if (string.IsNullOrEmpty(cname))\n                return Task.FromResult<DnsDatagram>(null);\n\n            IReadOnlyList<DnsResourceRecord> answers;\n\n            if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME\n            else\n                answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) };\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { 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.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"public\"\": \"\"api.example.com\"\",\n  \"\"private\"\": \"\"api.example.corp\"\",\n  \"\"custom-networks\"\": \"\"custom.example.corp\"\",\n  \"\"10.0.0.0/8\"\": \"\"api.intranet.example.corp\"\"\n}\";\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/SplitHorizonApp/SplitHorizonApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Version>10.0</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>SplitHorizonApp</AssemblyName>\n\t\t<RootNamespace>SplitHorizon</RootNamespace>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<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 &amp; AAAA type request based on the client's network address or configured domain names, and the configured 1:1 translation.</Description>\n\t\t<GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n\t\t<OutputType>Library</OutputType>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n\t\t\t<Private>false</Private>\n\t\t</ProjectReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"dnsApp.config\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/SplitHorizonApp/dnsApp.config",
    "content": "{\n    \"appPreference\": 40,\n    \"networks\": {\n        \"custom-networks\": [\n            \"172.16.1.0/24\",\n            \"172.16.10.0/24\",\n            \"172.16.2.1\"\n        ]\n    },\n    \"enableAddressTranslation\": false,\n    \"domainGroupMap\": {\n        \"example.com\": \"local1\"\n    },\n    \"networkGroupMap\": {\n        \"10.0.0.0/8\": \"local1\",\n        \"172.16.0.0/12\": \"local2\",\n        \"192.168.0.0/16\": \"local3\"\n    },\n    \"groups\": [\n        {\n            \"name\": \"local1\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.0/24\": \"10.0.0.0/24\",\n               \"5.6.7.8\": \"10.0.0.5\"\n            }\n        },\n        {\n            \"name\": \"local2\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"172.16.0.4\",\n               \"5.6.7.8\": \"172.16.0.5\"\n            }\n        },\n        {\n            \"name\": \"local3\",\n            \"enabled\": true,\n            \"translateReverseLookups\": true,\n            \"externalToInternalTranslation\": {\n               \"1.2.3.4\": \"192.168.0.4\",\n               \"5.6.7.8\": \"192.168.0.5\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "Apps/WeightedRoundRobinApp/Address.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Security.Cryptography;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace WeightedRoundRobin\n{\n    public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            string jsonPropertyName;\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                    jsonPropertyName = \"ipv4Addresses\";\n                    break;\n\n                case DnsResourceRecordType.AAAA:\n                    jsonPropertyName = \"ipv6Addresses\";\n                    break;\n\n                default:\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n\n            List<WeightedAddress> addresses;\n            int totalWeight = 0;\n\n            using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))\n            {\n                JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n                if (!jsonAppRecordData.TryGetProperty(jsonPropertyName, out JsonElement jsonAddresses) || (jsonAddresses.ValueKind == JsonValueKind.Null))\n                    return Task.FromResult<DnsDatagram>(null);\n\n                addresses = new List<WeightedAddress>(jsonAddresses.GetArrayLength());\n\n                foreach (JsonElement jsonAddressEntry in jsonAddresses.EnumerateArray())\n                {\n                    if (jsonAddressEntry.TryGetProperty(\"enabled\", out JsonElement jsonEnabled) && (jsonEnabled.ValueKind != JsonValueKind.Null) && !jsonEnabled.GetBoolean())\n                        continue;\n\n                    if (!jsonAddressEntry.TryGetProperty(\"address\", out JsonElement jsonAddress) || (jsonAddress.ValueKind == JsonValueKind.Null) || !IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address))\n                        continue;\n\n                    if (!jsonAddressEntry.TryGetProperty(\"weight\", out JsonElement jsonWeight) || (jsonWeight.ValueKind == JsonValueKind.Null))\n                        continue;\n\n                    int weight = jsonWeight.GetInt32();\n                    if (weight < 1)\n                        continue;\n\n                    addresses.Add(new WeightedAddress() { Address = address, Weight = weight });\n                    totalWeight += weight;\n                }\n            }\n\n            if (addresses.Count == 0)\n                return Task.FromResult<DnsDatagram>(null);\n\n            int randomSelection = RandomNumberGenerator.GetInt32(1, 101);\n            int rangeFrom;\n            int rangeTo = 0;\n            DnsResourceRecord answer = null;\n\n            for (int i = 0; i < addresses.Count; i++)\n            {\n                rangeFrom = rangeTo + 1;\n\n                if (i == addresses.Count - 1)\n                    rangeTo = 100;\n                else\n                    rangeTo += addresses[i].Weight * 100 / totalWeight;\n\n                if ((rangeFrom <= randomSelection) && (randomSelection <= rangeTo))\n                {\n                    switch (question.Type)\n                    {\n                        case DnsResourceRecordType.A:\n                            answer = new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, appRecordTtl, new DnsARecordData(addresses[i].Address));\n                            break;\n\n                        case DnsResourceRecordType.AAAA:\n                            answer = new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(addresses[i].Address));\n                            break;\n\n                        default:\n                            throw new InvalidOperationException();\n                    }\n\n                    break;\n                }\n            }\n\n            if (answer is null)\n                throw new InvalidOperationException();\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer }));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Returns an A or AAAA record using weighted round-robin load balancing.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"ipv4Addresses\"\": [\n    {\n       \"\"address\"\": \"\"1.1.1.1\"\",\n       \"\"weight\"\": 5,\n       \"\"enabled\"\": true\n    },\n    {\n       \"\"address\"\": \"\"2.2.2.2\"\",\n       \"\"weight\"\": 3,\n       \"\"enabled\"\": true\n    }\n  ],\n  \"\"ipv6Addresses\"\": [\n    {\n       \"\"address\"\": \"\"::1\"\",\n       \"\"weight\"\": 2,\n       \"\"enabled\"\": true\n    },\n    {\n       \"\"address\"\": \"\"::2\"\",\n       \"\"weight\"\": 3,\n       \"\"enabled\"\": true\n    }\n  ]\n}\";\n            }\n        }\n\n        #endregion\n\n        struct WeightedAddress\n        {\n            public IPAddress Address;\n            public int Weight;\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/WeightedRoundRobinApp/CNAME.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Security.Cryptography;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace WeightedRoundRobin\n{\n    public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            List<WeightedDomain> domainNames;\n            int totalWeight = 0;\n\n            using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))\n            {\n                JsonElement jsonAppRecordData = jsonDocument.RootElement;\n\n                if (!jsonAppRecordData.TryGetProperty(\"cnames\", out JsonElement jsonCnames) || (jsonCnames.ValueKind == JsonValueKind.Null))\n                    return Task.FromResult<DnsDatagram>(null);\n\n                domainNames = new List<WeightedDomain>(jsonCnames.GetArrayLength());\n\n                foreach (JsonElement jsonCnameEntry in jsonCnames.EnumerateArray())\n                {\n                    if (jsonCnameEntry.TryGetProperty(\"enabled\", out JsonElement jsonEnabled) && (jsonEnabled.ValueKind != JsonValueKind.Null) && !jsonEnabled.GetBoolean())\n                        continue;\n\n                    if (!jsonCnameEntry.TryGetProperty(\"domain\", out JsonElement jsonDomain) || (jsonDomain.ValueKind == JsonValueKind.Null))\n                        continue;\n\n                    if (!jsonCnameEntry.TryGetProperty(\"weight\", out JsonElement jsonWeight) || (jsonWeight.ValueKind == JsonValueKind.Null))\n                        continue;\n\n                    int weight = jsonWeight.GetInt32();\n                    if (weight < 1)\n                        continue;\n\n                    domainNames.Add(new WeightedDomain() { Domain = jsonDomain.GetString(), Weight = weight });\n                    totalWeight += weight;\n                }\n            }\n\n            if (domainNames.Count == 0)\n                return Task.FromResult<DnsDatagram>(null);\n\n            int randomSelection = RandomNumberGenerator.GetInt32(1, 101);\n            int rangeFrom;\n            int rangeTo = 0;\n            DnsResourceRecord answer = null;\n\n            for (int i = 0; i < domainNames.Count; i++)\n            {\n                rangeFrom = rangeTo + 1;\n\n                if (i == domainNames.Count - 1)\n                    rangeTo = 100;\n                else\n                    rangeTo += domainNames[i].Weight * 100 / totalWeight;\n\n                if ((rangeFrom <= randomSelection) && (randomSelection <= rangeTo))\n                {\n                    if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex\n                        answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(domainNames[i].Domain)); //use ANAME\n                    else\n                        answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(domainNames[i].Domain));\n\n                    break;\n                }\n            }\n\n            if (answer is null)\n                throw new InvalidOperationException();\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer }));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Returns a CNAME record using weighted round-robin load balancing.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        {\n            get\n            {\n                return @\"{\n  \"\"cnames\"\": [\n    {\n       \"\"domain\"\": \"\"example.com\"\",\n       \"\"weight\"\": 5,\n       \"\"enabled\"\": true\n    },\n    {\n       \"\"domain\"\": \"\"example.net\"\",\n       \"\"weight\"\": 3,\n       \"\"enabled\"\": true\n    }\n  ]\n}\";\n            }\n        }\n\n        #endregion\n\n        struct WeightedDomain\n        {\n            public string Domain;\n            public int Weight;\n        }\n    }\n}\n"
  },
  {
    "path": "Apps/WeightedRoundRobinApp/WeightedRoundRobinApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>WeightedRoundRobinApp</AssemblyName>\n    <RootNamespace>WeightedRoundRobin</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <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.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/WeightedRoundRobinApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/WhatIsMyDnsApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace WhatIsMyDns\n{\n    public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            //do nothing\n            return Task.CompletedTask;\n        }\n\n        public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*'))\n                return Task.FromResult<DnsDatagram>(null);\n\n            DnsResourceRecord answer;\n\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                    if (remoteEP.AddressFamily != AddressFamily.InterNetwork)\n                        return Task.FromResult<DnsDatagram>(null);\n\n                    answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(remoteEP.Address));\n                    break;\n\n                case DnsResourceRecordType.AAAA:\n                    if (remoteEP.AddressFamily != AddressFamily.InterNetworkV6)\n                        return Task.FromResult<DnsDatagram>(null);\n\n                    answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(remoteEP.Address));\n                    break;\n\n                case DnsResourceRecordType.TXT:\n                    answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, DnsClass.IN, appRecordTtl, new DnsTXTRecordData(remoteEP.Address.ToString()));\n                    break;\n\n                default:\n                    return Task.FromResult<DnsDatagram>(null);\n            }\n\n            return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer }));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Returns the IP address of the user's DNS Server for A, AAAA, and TXT queries.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        { get { return null; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/WhatIsMyDnsApp/WhatIsMyDnsApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    <Version>8.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>WhatIsMyDnsApp</AssemblyName>\n    <RootNamespace>WhatIsMyDns</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <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.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/WhatIsMyDnsApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/WildIpApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace WildIp\n{\n    public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler\n    {\n        #region variables\n\n        static readonly char[] aRecordSeparator = new char[] { '.', '-' };\n        static readonly char[] aaaaRecordSeparator = new char[] { '.' };\n\n        IDnsServer _dnsServer;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData)\n        {\n            string qname = request.Question[0].Name;\n\n            if (qname.Length == appRecordName.Length)\n                return null;\n\n            DnsResourceRecord answer = null;\n\n            switch (request.Question[0].Type)\n            {\n                case DnsResourceRecordType.A:\n                    {\n                        string subdomain = qname.Substring(0, qname.Length - appRecordName.Length);\n                        string[] parts = subdomain.Split(aRecordSeparator, StringSplitOptions.RemoveEmptyEntries);\n                        byte[] rawIp = new byte[4];\n                        int i = 0;\n\n                        for (int j = 0; (j < parts.Length) && (i < 4); j++)\n                        {\n                            if (byte.TryParse(parts[j], out byte x))\n                                rawIp[i++] = x;\n                        }\n\n                        if (i == 4)\n                            answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(new IPAddress(rawIp)));\n                    }\n                    break;\n\n                case DnsResourceRecordType.AAAA:\n                    {\n                        string subdomain = qname.Substring(0, qname.Length - appRecordName.Length - 1);\n                        string[] parts = subdomain.Split(aaaaRecordSeparator, StringSplitOptions.RemoveEmptyEntries);\n                        IPAddress address = null;\n\n                        foreach (string part in parts)\n                        {\n                            if (part.Contains('-') && IPAddress.TryParse(part.Replace('-', ':'), out address))\n                            {\n                                break;\n                            }\n                            else if (part.Length == 32)\n                            {\n                                string addr = null;\n\n                                for (int i = 0; i < 32; i += 4)\n                                {\n                                    if (addr is null)\n                                        addr = part.Substring(i, 4);\n                                    else\n                                        addr += string.Concat(\":\", part.AsSpan(i, 4));\n                                }\n\n                                if (IPAddress.TryParse(addr, out address))\n                                    break;\n                            }\n                        }\n\n                        if (address is not null)\n                            answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address));\n                    }\n                    break;\n            }\n\n            if (answer is null)\n            {\n                //NODATA reponse\n                DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN));\n\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer);\n            }\n\n            return new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer });\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.\"; } }\n\n        public string ApplicationRecordDataTemplate\n        { get { return null; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/WildIpApp/WildIpApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    <Version>5.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>WildIpApp</AssemblyName>\n    <RootNamespace>WildIp</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <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.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/WildIpApp/dnsApp.config",
    "content": "#This app requires no config."
  },
  {
    "path": "Apps/ZoneAliasApp/App.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace ZoneAlias\n{\n    public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference\n    {\n        #region variables\n\n        IDnsServer _dnsServer;\n\n        byte _appPreference;\n\n        bool _enableAliasing;\n        Dictionary<string, string> _aliases;\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            //do nothing\n        }\n\n        #endregion\n\n        #region private\n\n        private static string GetParentZone(string domain)\n        {\n            int i = domain.IndexOf('.');\n            if (i > -1)\n                return domain.Substring(i + 1);\n\n            //dont return root zone\n            return null;\n        }\n\n        private bool IsZoneAlias(string domain, out string zone, out string alias)\n        {\n            domain = domain.ToLowerInvariant();\n\n            do\n            {\n                if (_aliases.TryGetValue(domain, out zone))\n                {\n                    //found alias\n                    alias = domain;\n                    return true;\n                }\n\n                domain = GetParentZone(domain);\n            }\n            while (domain is not null);\n\n            alias = null;\n            return false;\n        }\n\n        private static IReadOnlyList<DnsResourceRecord> ConvertRecords(IReadOnlyList<DnsResourceRecord> records, string zone, string alias)\n        {\n            if (records.Count == 0)\n                return records;\n\n            DnsResourceRecord[] newRecords = new DnsResourceRecord[records.Count];\n            int j;\n\n            for (int i = 0; i < records.Count; i++)\n            {\n                DnsResourceRecord record = records[i];\n\n                j = record.Name.LastIndexOf(zone, StringComparison.OrdinalIgnoreCase);\n                if (j == 0)\n                    newRecords[i] = new DnsResourceRecord(alias, record.Type, record.Class, record.TTL, record.RDATA);\n                else if (j > 0)\n                    newRecords[i] = new DnsResourceRecord(string.Concat(record.Name.AsSpan(0, j), alias), record.Type, record.Class, record.TTL, record.RDATA);\n                else\n                    newRecords[i] = record;\n            }\n\n            return newRecords;\n        }\n\n        #endregion\n\n        #region public\n\n        public Task InitializeAsync(IDnsServer dnsServer, string config)\n        {\n            _dnsServer = dnsServer;\n\n            using JsonDocument jsonDocument = JsonDocument.Parse(config);\n            JsonElement jsonConfig = jsonDocument.RootElement;\n\n            _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue(\"appPreference\", 10));\n\n            _enableAliasing = jsonConfig.GetPropertyValue(\"enableAliasing\", true);\n\n            if (jsonConfig.TryGetProperty(\"zoneAliases\", out JsonElement jsonZoneAliases))\n            {\n                Dictionary<string, string> aliases = new Dictionary<string, string>();\n\n                foreach (JsonProperty jsonZoneAlias in jsonZoneAliases.EnumerateObject())\n                {\n                    string zone = jsonZoneAlias.Name.ToLowerInvariant();\n\n                    foreach (JsonElement jsonAlias in jsonZoneAlias.Value.EnumerateArray())\n                        aliases.Add(jsonAlias.GetString().ToLowerInvariant(), zone);\n                }\n\n                aliases.TrimExcess();\n\n                _aliases = aliases;\n            }\n            else\n            {\n                _aliases = null;\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)\n        {\n            if (!_enableAliasing || (_aliases is null))\n                return null;\n\n            DnsQuestionRecord question = request.Question[0];\n            string qname = question.Name;\n\n            if (!IsZoneAlias(qname, out string zone, out string alias))\n                return null;\n\n            string newQname;\n            int i = qname.LastIndexOf(alias, StringComparison.OrdinalIgnoreCase);\n            if (i == 0)\n                newQname = zone;\n            else if (i > 0)\n                newQname = string.Concat(qname.AsSpan(0, i), zone);\n            else\n                return null;\n\n            DnsQuestionRecord newQuestion = new DnsQuestionRecord(newQname, question.Type, question.Class);\n\n            try\n            {\n                DnsDatagram response = await _dnsServer.DirectQueryAsync(newQuestion);\n\n                IReadOnlyList<DnsResourceRecord> newAnswer = ConvertRecords(response.Answer, zone, alias);\n                IReadOnlyList<DnsResourceRecord> newAuthority = ConvertRecords(response.Authority, zone, alias);\n                IReadOnlyList<DnsResourceRecord> newAdditional = ConvertRecords(response.Additional, zone, alias);\n\n                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 };\n            }\n            catch (TimeoutException)\n            { }\n            catch (Exception ex)\n            {\n                _dnsServer.WriteLog(ex);\n            }\n\n            return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.ServerFailure, request.Question);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Description\n        { get { return \"Allows configuring aliases for any zone (internal or external) such that they all return the same set of records.\"; } }\n\n        public byte Preference\n        { get { return _appPreference; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "Apps/ZoneAliasApp/ZoneAliasApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Version>4.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <Authors>Shreyas Zare</Authors>\n    <AssemblyName>ZoneAliasApp</AssemblyName>\n    <RootNamespace>ZoneAlias</RootNamespace>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <Description>Allows configuring aliases for any zone (internal or external) such that they all return the same set of records.</Description>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <OutputType>Library</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\">\n      <Private>false</Private>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"dnsApp.config\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Apps/ZoneAliasApp/dnsApp.config",
    "content": "{\n  \"appPreference\": 10,\n  \"enableAliasing\": true,\n  \"zoneAliases\": {\n    \"example.com\": [\"example.net\", \"example.org\"]\n  }\n}\n"
  },
  {
    "path": "Apps/apps2.json",
    "content": "[\n\t{\n\t\t\"name\": \"Advanced Blocking\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v3.zip\",\n\t\t\t\t\"size\": \"28.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v4.zip\",\n\t\t\t\t\"size\": \"28.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"4.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v4.0.1.zip\",\n\t\t\t\t\"size\": \"28.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"27.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"5.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v5.1.zip\",\n\t\t\t\t\"size\": \"27.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.1\",\n\t\t\t\t\"version\": \"5.1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v5.1.1.zip\",\n\t\t\t\t\"size\": \"27.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v6.zip\",\n\t\t\t\t\"size\": \"28.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5.1\",\n\t\t\t\t\"version\": \"6.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v6.0.1.zip\",\n\t\t\t\t\"size\": \"28.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5.3\",\n\t\t\t\t\"version\": \"6.1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v6.1.1.zip\",\n\t\t\t\t\"size\": \"29.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v7.zip\",\n\t\t\t\t\"size\": \"30.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.1\",\n\t\t\t\t\"version\": \"7.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v7.1.zip\",\n\t\t\t\t\"size\": \"30.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.2\",\n\t\t\t\t\"version\": \"7.1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v7.1.1.zip\",\n\t\t\t\t\"size\": \"30.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v8.zip\",\n\t\t\t\t\"size\": \"30.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"9.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v9.zip\",\n\t\t\t\t\"size\": \"31.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.2.0\",\n\t\t\t\t\"version\": \"9.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v9.1.zip\",\n\t\t\t\t\"size\": \"33.08 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"10.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedBlockingApp-v10.zip\",\n\t\t\t\t\"size\": \"33.12 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Advanced Forwarding\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"1.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.0.1.zip\",\n\t\t\t\t\"size\": \"19.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"1.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.0.2.zip\",\n\t\t\t\t\"size\": \"19.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.1\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.1.zip\",\n\t\t\t\t\"size\": \"19.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5\",\n\t\t\t\t\"version\": \"1.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.2.zip\",\n\t\t\t\t\"size\": \"19.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v2.zip\",\n\t\t\t\t\"size\": \"20.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.1\",\n\t\t\t\t\"version\": \"2.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v2.1.zip\",\n\t\t\t\t\"size\": \"20.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v3.zip\",\n\t\t\t\t\"size\": \"20.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"3.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v3.1.zip\",\n\t\t\t\t\"size\": \"20.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AdvancedForwardingApp-v4.zip\",\n\t\t\t\t\"size\": \"20.7 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Auto PTR\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AutoPtrApp-v1.zip\",\n\t\t\t\t\"size\": \"10.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"1.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AutoPtrApp-v1.0.2.zip\",\n\t\t\t\t\"size\": \"11.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AutoPtrApp-v2.zip\",\n\t\t\t\t\"size\": \"12.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AutoPtrApp-v3.zip\",\n\t\t\t\t\"size\": \"12.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/AutoPtrApp-v4.zip\",\n\t\t\t\t\"size\": \"12.3 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Block Page\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v2.zip\",\n\t\t\t\t\"size\": \"21.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v3.zip\",\n\t\t\t\t\"size\": \"22.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"3.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v3.0.1.zip\",\n\t\t\t\t\"size\": \"22.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v4.zip\",\n\t\t\t\t\"size\": \"20.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"4.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v4.1.zip\",\n\t\t\t\t\"size\": \"20.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"4.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v4.2.zip\",\n\t\t\t\t\"size\": \"21.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5.3\",\n\t\t\t\t\"version\": \"4.3\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v4.3.zip\",\n\t\t\t\t\"size\": \"23.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5.3\",\n\t\t\t\t\"version\": \"4.3.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v4.3.1.zip\",\n\t\t\t\t\"size\": \"23.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v5.zip\",\n\t\t\t\t\"size\": \"25.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.2\",\n\t\t\t\t\"version\": \"5.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v5.1.zip\",\n\t\t\t\t\"size\": \"25.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v6.zip\",\n\t\t\t\t\"size\": \"25.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.1.1\",\n\t\t\t\t\"version\": \"6.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v6.0.1.zip\",\n\t\t\t\t\"size\": \"25.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4.1\",\n\t\t\t\t\"version\": \"6.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v6.1.zip\",\n\t\t\t\t\"size\": \"28.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4.2\",\n\t\t\t\t\"version\": \"6.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v6.2.zip\",\n\t\t\t\t\"size\": \"28.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v7.zip\",\n\t\t\t\t\"size\": \"28.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"7.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/BlockPageApp-v7.1.zip\",\n\t\t\t\t\"size\": \"28.88 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Default Records\",\n\t\t\"description\": \"Allows setting one or more default records for configured local zones.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DefaultRecordsApp-v1.zip\",\n\t\t\t\t\"size\": \"13.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DefaultRecordsApp-v2.zip\",\n\t\t\t\t\"size\": \"14.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DefaultRecordsApp-v3.zip\",\n\t\t\t\t\"size\": \"13.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DefaultRecordsApp-v4.zip\",\n\t\t\t\t\"size\": \"14.0 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS64\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/Dns64App-v1.zip\",\n\t\t\t\t\"size\": \"15.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"1.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/Dns64App-v1.0.1.zip\",\n\t\t\t\t\"size\": \"15.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/Dns64App-v2.zip\",\n\t\t\t\t\"size\": \"14.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/Dns64App-v3.zip\",\n\t\t\t\t\"size\": \"15.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/Dns64App-v4.zip\",\n\t\t\t\t\"size\": \"15.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"4.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/Dns64App-v4.1.zip\",\n\t\t\t\t\"size\": \"15.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/Dns64App-v5.zip\",\n\t\t\t\t\"size\": \"15.7 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS Block List (DNSBL)\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsBlockListApp-v1.zip\",\n\t\t\t\t\"size\": \"18.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"1.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsBlockListApp-v1.0.1.zip\",\n\t\t\t\t\"size\": \"18.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsBlockListApp-v2.zip\",\n\t\t\t\t\"size\": \"19.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsBlockListApp-v3.zip\",\n\t\t\t\t\"size\": \"19.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsBlockListApp-v4.zip\",\n\t\t\t\t\"size\": \"19.7 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS Rebinding Protection\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v1.1.zip\",\n\t\t\t\t\"size\": \"11.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v2.zip\",\n\t\t\t\t\"size\": \"11.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.1.1\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v3.zip\",\n\t\t\t\t\"size\": \"12.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v4.zip\",\n\t\t\t\t\"size\": \"13.0 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Drop Requests\",\n\t\t\"description\": \"Drops incoming DNS requests that match list of blocked networks or blocked questions.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"2.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v2.1.zip\",\n\t\t\t\t\"size\": \"13.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v3.zip\",\n\t\t\t\t\"size\": \"13.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"3.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v3.0.1.zip\",\n\t\t\t\t\"size\": \"13.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v4.zip\",\n\t\t\t\t\"size\": \"12.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v5.zip\",\n\t\t\t\t\"size\": \"12.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v6.zip\",\n\t\t\t\t\"size\": \"12.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4\",\n\t\t\t\t\"version\": \"6.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v6.1.zip\",\n\t\t\t\t\"size\": \"12.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/DropRequestsApp-v7.zip\",\n\t\t\t\t\"size\": \"12.7 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Failover\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v4.zip\",\n\t\t\t\t\"size\": \"52.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v5.zip\",\n\t\t\t\t\"size\": \"53.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"5.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v5.1.zip\",\n\t\t\t\t\"size\": \"53.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v6.zip\",\n\t\t\t\t\"size\": \"47.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"6.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v6.0.1.zip\",\n\t\t\t\t\"size\": \"47.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.1\",\n\t\t\t\t\"version\": \"6.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v6.1.zip\",\n\t\t\t\t\"size\": \"47.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"6.1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v6.1.1.zip\",\n\t\t\t\t\"size\": \"48.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"6.1.3\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v6.1.3.zip\",\n\t\t\t\t\"size\": \"48.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5\",\n\t\t\t\t\"version\": \"6.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v6.2.zip\",\n\t\t\t\t\"size\": \"48.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v7.zip\",\n\t\t\t\t\"size\": \"49.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.1\",\n\t\t\t\t\"version\": \"7.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v7.0.1.zip\",\n\t\t\t\t\"size\": \"49.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v8.zip\",\n\t\t\t\t\"size\": \"50.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.2.1\",\n\t\t\t\t\"version\": \"8.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v8.0.1.zip\",\n\t\t\t\t\"size\": \"50.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"9.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v9.zip\",\n\t\t\t\t\"size\": \"50.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"9.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FailoverApp-v9.1.zip\",\n\t\t\t\t\"size\": \"50.54 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Filter AAAA\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.2\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FilterAaaaApp-v1.zip\",\n\t\t\t\t\"size\": \"12.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FilterAaaaApp-v2.zip\",\n\t\t\t\t\"size\": \"12.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.1\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FilterAaaaApp-v3.zip\",\n\t\t\t\t\"size\": \"13.8 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.1.1\",\n\t\t\t\t\"version\": \"3.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FilterAaaaApp-v3.1.zip\",\n\t\t\t\t\"size\": \"14.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.2\",\n\t\t\t\t\"version\": \"3.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FilterAaaaApp-v3.2.zip\",\n\t\t\t\t\"size\": \"14.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/FilterAaaaApp-v4.zip\",\n\t\t\t\t\"size\": \"14.3 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Geo Continent\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v4.zip\",\n\t\t\t\t\"size\": \"3.00 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v5.zip\",\n\t\t\t\t\"size\": \"2.76 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"2.76 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v6.zip\",\n\t\t\t\t\"size\": \"2.76 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"6.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v6.0.1.zip\",\n\t\t\t\t\"size\": \"2.92 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"6.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v6.0.2.zip\",\n\t\t\t\t\"size\": \"2.92 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"6.0.4\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v6.0.4.zip\",\n\t\t\t\t\"size\": \"2.92 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v7.zip\",\n\t\t\t\t\"size\": \"3.16 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.1\",\n\t\t\t\t\"version\": \"7.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v7.1.zip\",\n\t\t\t\t\"size\": \"7.57 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v8.zip\",\n\t\t\t\t\"size\": \"8.35 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.6\",\n\t\t\t\t\"version\": \"8.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v8.1.zip\",\n\t\t\t\t\"size\": \"8.35 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"9.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v9.zip\",\n\t\t\t\t\"size\": \"10.5 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"9.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoContinentApp-v9.0.1.zip\",\n\t\t\t\t\"size\": \"10.59 MB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Geo Country\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v4.zip\",\n\t\t\t\t\"size\": \"3.00 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v5.zip\",\n\t\t\t\t\"size\": \"2.76 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"2.76 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v6.zip\",\n\t\t\t\t\"size\": \"2.76 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"6.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v6.0.1.zip\",\n\t\t\t\t\"size\": \"2.92 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"6.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v6.0.2.zip\",\n\t\t\t\t\"size\": \"2.92 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"6.0.4\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v6.0.4.zip\",\n\t\t\t\t\"size\": \"2.92 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v7.zip\",\n\t\t\t\t\"size\": \"3.16 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.1\",\n\t\t\t\t\"version\": \"7.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v7.1.zip\",\n\t\t\t\t\"size\": \"7.57 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v8.zip\",\n\t\t\t\t\"size\": \"8.35 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.6\",\n\t\t\t\t\"version\": \"8.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v8.1.zip\",\n\t\t\t\t\"size\": \"8.35 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"9.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v9.zip\",\n\t\t\t\t\"size\": \"10.5 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"9.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoCountryApp-v9.0.1.zip\",\n\t\t\t\t\"size\": \"10.59 MB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Geo Distance\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v4.zip\",\n\t\t\t\t\"size\": \"32.7 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v5.zip\",\n\t\t\t\t\"size\": \"32.5 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"32.5 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v6.zip\",\n\t\t\t\t\"size\": \"32.5 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"6.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v6.0.1.zip\",\n\t\t\t\t\"size\": \"33.9 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"6.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v6.0.2.zip\",\n\t\t\t\t\"size\": \"33.9 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"6.0.4\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v6.0.4.zip\",\n\t\t\t\t\"size\": \"33.9 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v7.zip\",\n\t\t\t\t\"size\": \"31.0 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.1\",\n\t\t\t\t\"version\": \"7.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v7.1.zip\",\n\t\t\t\t\"size\": \"35.4 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v8.zip\",\n\t\t\t\t\"size\": \"33.4 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"9.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v9.zip\",\n\t\t\t\t\"size\": \"35.5 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"9.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/GeoDistanceApp-v9.0.1.zip\",\n\t\t\t\t\"size\": \"35.59 MB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Log Exporter\",\n\t\t\"description\": \"Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols).\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/LogExporterApp-v1.zip\",\n\t\t\t\t\"size\": \"202 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"1.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/LogExporterApp-v1.0.1.zip\",\n\t\t\t\t\"size\": \"202 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.6\",\n\t\t\t\t\"version\": \"1.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/LogExporterApp-v1.0.2.zip\",\n\t\t\t\t\"size\": \"202 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/LogExporterApp-v2.zip\",\n\t\t\t\t\"size\": \"212 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.2.0\",\n\t\t\t\t\"version\": \"2.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/LogExporterApp-v2.1.zip\",\n\t\t\t\t\"size\": \"213.95 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"MISP Connector\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.2.0\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/MispConnectorApp-v1.zip\",\n\t\t\t\t\"size\": \"24.49 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"NO DATA\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v1.zip\",\n\t\t\t\t\"size\": \"11.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"1.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v1.0.1.zip\",\n\t\t\t\t\"size\": \"11.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v2.zip\",\n\t\t\t\t\"size\": \"10.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"2.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v2.0.1.zip\",\n\t\t\t\t\"size\": \"10.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"2.0.3\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v2.0.3.zip\",\n\t\t\t\t\"size\": \"10.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v3.zip\",\n\t\t\t\t\"size\": \"10.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v4.zip\",\n\t\t\t\t\"size\": \"10.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NoDataApp-v5.zip\",\n\t\t\t\t\"size\": \"10.8 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"NX Domain\",\n\t\t\"description\": \"Blocks configured domain names with a NX Domain response.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v2.zip\",\n\t\t\t\t\"size\": \"11.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v3.zip\",\n\t\t\t\t\"size\": \"12.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"3.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v3.0.1.zip\",\n\t\t\t\t\"size\": \"12.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v4.zip\",\n\t\t\t\t\"size\": \"10.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v5.zip\",\n\t\t\t\t\"size\": \"11.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.2\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"11.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v6.zip\",\n\t\t\t\t\"size\": \"11.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"6.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v6.1.zip\",\n\t\t\t\t\"size\": \"11.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainApp-v7.zip\",\n\t\t\t\t\"size\": \"13.6 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"NX Domain Override\",\n\t\t\"description\": \"Overrides NX Domain response with custom A/AAAA record response for configured domain names.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainOverrideApp-v1.zip\",\n\t\t\t\t\"size\": \"13.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainOverrideApp-v2.zip\",\n\t\t\t\t\"size\": \"12.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/NxDomainOverrideApp-v3.zip\",\n\t\t\t\t\"size\": \"13.0 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Query Logs (MySQL)\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v1.zip\",\n\t\t\t\t\"size\": \"6.70 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4.1\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v1.1.zip\",\n\t\t\t\t\"size\": \"6.70 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4.2\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v2.zip\",\n\t\t\t\t\"size\": \"472 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"2.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v2.0.1.zip\",\n\t\t\t\t\"size\": \"472 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v3.zip\",\n\t\t\t\t\"size\": \"478 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"3.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v3.0.1.zip\",\n\t\t\t\t\"size\": \"482.76 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Query Logs (Sqlite)\",\n\t\t\"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).\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"2.0.4\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v2.0.4.zip\",\n\t\t\t\t\"size\": \"9.39 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v3.zip\",\n\t\t\t\t\"size\": \"13.0 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"3.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v3.1.zip\",\n\t\t\t\t\"size\": \"13.0 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.zip\",\n\t\t\t\t\"size\": \"13.7 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.1\",\n\t\t\t\t\"version\": \"4.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.0.1.zip\",\n\t\t\t\t\"size\": \"13.6 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"4.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.0.2.zip\",\n\t\t\t\t\"size\": \"13.6 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5.2\",\n\t\t\t\t\"version\": \"4.0.3\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.0.3.zip\",\n\t\t\t\t\"size\": \"13.6 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5.3\",\n\t\t\t\t\"version\": \"4.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.1.zip\",\n\t\t\t\t\"size\": \"13.7 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v5.zip\",\n\t\t\t\t\"size\": \"12.2 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.1\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"12.2 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.2\",\n\t\t\t\t\"version\": \"5.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v5.0.2.zip\",\n\t\t\t\t\"size\": \"12.2 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v6.zip\",\n\t\t\t\t\"size\": \"12.2 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.3\",\n\t\t\t\t\"version\": \"6.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v6.1.zip\",\n\t\t\t\t\"size\": \"14.3 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v7.zip\",\n\t\t\t\t\"size\": \"14.3 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.6\",\n\t\t\t\t\"version\": \"7.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v7.1.zip\",\n\t\t\t\t\"size\": \"14.3 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v8.zip\",\n\t\t\t\t\"size\": \"14.6 MB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Query Logs (SQL Server)\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.zip\",\n\t\t\t\t\"size\": \"4.67 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4.1\",\n\t\t\t\t\"version\": \"1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.1.zip\",\n\t\t\t\t\"size\": \"4.67 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.4.2\",\n\t\t\t\t\"version\": \"1.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.2.zip\",\n\t\t\t\t\"size\": \"4.67 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"1.2.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.2.1.zip\",\n\t\t\t\t\"size\": \"4.67 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.6\",\n\t\t\t\t\"version\": \"1.2.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.2.2.zip\",\n\t\t\t\t\"size\": \"4.67 MB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v2.zip\",\n\t\t\t\t\"size\": \"4.82 MB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Split Horizon\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v4.zip\",\n\t\t\t\t\"size\": \"14.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v5.zip\",\n\t\t\t\t\"size\": \"19.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0.1\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"19.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v6.zip\",\n\t\t\t\t\"size\": \"17.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"6.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v6.0.1.zip\",\n\t\t\t\t\"size\": \"17.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"6.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v6.0.2.zip\",\n\t\t\t\t\"size\": \"17.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"6.0.4\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v6.0.4.zip\",\n\t\t\t\t\"size\": \"17.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.5\",\n\t\t\t\t\"version\": \"6.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v6.1.zip\",\n\t\t\t\t\"size\": \"18.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v7.zip\",\n\t\t\t\t\"size\": \"19.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v8.zip\",\n\t\t\t\t\"size\": \"19.4 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"8.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v8.1.zip\",\n\t\t\t\t\"size\": \"19.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"9.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v9.zip\",\n\t\t\t\t\"size\": \"19.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.3\",\n\t\t\t\t\"version\": \"10.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/SplitHorizonApp-v10.zip\",\n\t\t\t\t\"size\": \"20.27 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Weighted Round Robin\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v1.zip\",\n\t\t\t\t\"size\": \"11.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"1.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v1.0.2.zip\",\n\t\t\t\t\"size\": \"12.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v2.zip\",\n\t\t\t\t\"size\": \"12.7 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v3.zip\",\n\t\t\t\t\"size\": \"12.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v4.zip\",\n\t\t\t\t\"size\": \"12.7 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"What Is My Dns\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v4.zip\",\n\t\t\t\t\"size\": \"9.26 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.zip\",\n\t\t\t\t\"size\": \"9.93 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"5.0.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.1.zip\",\n\t\t\t\t\"size\": \"9.77 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"5.0.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.2.zip\",\n\t\t\t\t\"size\": \"9.79 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"5.0.3\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.3.zip\",\n\t\t\t\t\"size\": \"9.87 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"5.0.5\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.5.zip\",\n\t\t\t\t\"size\": \"9.90 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"6.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v6.zip\",\n\t\t\t\t\"size\": \"10.6 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"7.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v7.zip\",\n\t\t\t\t\"size\": \"10.5 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"8.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v8.zip\",\n\t\t\t\t\"size\": \"10.5 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Wild IP\",\n\t\t\"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.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"9.0\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v1.zip\",\n\t\t\t\t\"size\": \"9.66 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"10.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v2.zip\",\n\t\t\t\t\"size\": \"10.3 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0\",\n\t\t\t\t\"version\": \"2.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v2.1.zip\",\n\t\t\t\t\"size\": \"11.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.0.3\",\n\t\t\t\t\"version\": \"2.1.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v2.1.1.zip\",\n\t\t\t\t\"size\": \"11.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.2\",\n\t\t\t\t\"version\": \"2.2\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v2.2.zip\",\n\t\t\t\t\"size\": \"11.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v3.zip\",\n\t\t\t\t\"size\": \"12.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v4.zip\",\n\t\t\t\t\"size\": \"12.1 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"5.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/WildIpApp-v5.zip\",\n\t\t\t\t\"size\": \"12.2 KB\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Zone Alias\",\n\t\t\"description\": \"Allows configuring aliases for any zone (internal or external) such that they all return the same set of records.\",\n\t\t\"versions\": [\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"11.3\",\n\t\t\t\t\"version\": \"1.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/ZoneAliasApp-v1.zip\",\n\t\t\t\t\"size\": \"12.2 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"12.0\",\n\t\t\t\t\"version\": \"2.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/ZoneAliasApp-v2.zip\",\n\t\t\t\t\"size\": \"13.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.0\",\n\t\t\t\t\"version\": \"3.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/ZoneAliasApp-v3.zip\",\n\t\t\t\t\"size\": \"12.9 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"13.5\",\n\t\t\t\t\"version\": \"3.1\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/ZoneAliasApp-v3.1.zip\",\n\t\t\t\t\"size\": \"13.0 KB\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"serverVersion\": \"14.0\",\n\t\t\t\t\"version\": \"4.0\",\n\t\t\t\t\"url\": \"https://download.technitium.com/dns/apps/ZoneAliasApp-v4.zip\",\n\t\t\t\t\"size\": \"13.2 KB\"\n\t\t\t}\n\t\t]\n\t}\n]"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Technitium DNS Server Change Log\n\n## Version 14.3\nRelease Date: 20 December 2025\n\n- Added support for Dark Mode. Thanks to @skidoodle for the PR.\n- Updated Catalog zones implementation to allow adding Secondary zones as members.\n- Updated Restore Settings option to allow importing backup zip files from older DNS server versions.\n- Added new options in Settings to configure default TTL values for NS and SOA records.\n- Added DNS record overwrite option in DHCP Scopes to allow dynamic leases to overwrite any existing DNS A record for the client domain name.\n- Advanced Blocking App: Added new option to allow configuring block list update interval in minutes.\n- Split Horizon App: Updated app to support mapping domain names to group for address translation feature.\n- Multiple other minor bug fixes and improvements.\n\n## Version 14.2\nRelease Date: 22 November 2025\n\n- Fixed bug in Clustering implementation which prevented using IPv4 and IPv6 addresses together. Thanks to @ruifung for the PR. \n- There is also a breaking change in clustering and thus all cluster nodes must be upgraded to this release to avoid issues.\n- Updated the \"Allow / Block List URLs\" option implementation to support comment entries.\n- Advanced Blocking App: Updated app to implement `blockingAnswerTtl` option to allow specifying the TTL value used in blocked response.\n- Log Exporter App: Updated the app to add EDNS logging support. Thanks to @zbalkan for the PR.\n- MISP Connector App: Added new app that can block malicious domain names pulled from MISP feeds. Thanks to @zbalkan for the PR.\n- Multiple other minor bug fixes and improvements.\n\n## Version 14.1\nRelease Date: 16 November 2025\n\n- 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.\n- Fixed issues related to user and group permission validation when Clustering is enabled which caused permission bypass when accessing another node.\n- Fixed bug that caused the Advanced Blocking app to stop working.\n- Added environment variables for TLS certificate path, certificate password, and HTTP to HTTPS redirect option. Thanks to @simonvandermeer for the PR.\n- Updated Hagezi block list URLs. Thanks to @hagezi for the PR.\n- Other minor changes and improvements.\n\n## Version 14.0.1\nRelease Date: 9 November 2025\n\n- Fixed bugs in the Force Update Block List and Temporary Disable Blocking API calls.\n- Fixed session validation bypass bug during proxying request to another node when Clustering is enabled.\n- Fixed issue of failing to load app config due to text encoding issues.\n- Fixed issue of failure to load old config file versions due to validation failures in some cases.\n- Updated GUI docs for Cluster initialization and joining.\n- Other minor changes and improvements.\n\n## Version 14.0\nRelease Date: 8 November 2025\n\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- Added TOTP based Two-factor authentication (2FA) support.\n- Added options to configure UDP Socket pooling feature in Settings.\n- 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.\n- Fixed issue with internal Http Client to retry for IPv4 addresses too when `Prefer IPv6` option is enabled and IPv6 address failed to connect.\n- Fixed bug of missing NSEC/NSEC3 record in response for wildcard and Empty Non-terminal (ENT) records in Primary zones.\n- Fixed multiple issues in Prefetch and Auto Prefetch implementation that caused undesirable frequent refreshing of cached data in certain cases.\n- Query Logs (Sqlite) App: Updated app to use Channels for better performance.\n- Query Logs (MySQL) App: Updated app to use Channels for better performance. Fixed bug in schema for protocol parameter causing overflow.\n- Query Logs (SQL Server) App: Updated app to use Channels for better performance.\n- NX Domain App: Updated app to support Extended DNS Error messages.\n- Multiple other minor bug fixes and improvements.\n \n## Version 13.6\nRelease Date: 26 April 2025\n\n- 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.\n- 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.\n- Updated the record filtering option in zone edit view to support wildcard based search.\n- Fixed issue in DNS-over-QUIC service that caused the service to stop working due to failed connection handshake.\n- Query Logs (Sqlite) App: Updated app to support VACCUM option to allow trimming database file on disk to reduce its size.\n- Geo Continent App and Geo Country App: Updated both apps to support macro variable to simplify APP record data JSON configuration.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.5\nRelease Date: 6 April 2025\n\n- 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.\n- 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.\n- Added feature to filter records in the zone editor based on its name or type to allow ease of searching records in large zones.\n- Added support for writing DNS logs to Console (STDOUT) along with existing option to write to a file.\n- 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.\n- Updated zone file parser to support BIND extended zone file format.\n- Updated Query Logs view to show records with background color based on the type of log entry.\n- 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.\n- 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.\n- Added `IDnsApplicationPreference` interface to allow applications to be ordered based on their user configured app preference value.\n- 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.\n- Log Exporter App: Updated app to allow configuring HTTP headers without validation to allow adding non-standard header values.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.4.3\nRelease Date: 23 February 2025\n\n- Fixed issue of high memory usage when \"Last Year\" option is used on Dashboard.\n- Fixed multiple issues of DNSSEC validation failures for certain domain names when using forwarders.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.4.2\nRelease Date: 15 February 2025\n\n- Fixed issue of unhandled CD flag condition when DO flag is unset in requests for a specific case.\n- Block Page App: Fixed issue with Kestrel local addresses that caused failure to bind on Linux systems.\n- Query Logs (MySQL) App: Updated app to use MySqlConnector driver which allows the app to work with MariaDB too.\n- Query Logs (SQL Server) App: Fixed issue with bulk insert due to limit on parameters per query. Fixed issue with qtype filtering.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.4.1\nRelease Date: 2 February 2025\n\n- Fixed issue of unhandled CD flag condition when DO flag is unset in requests.\n- Block Page App: Updated app to show blocking info details on the block page.\n- Query Logs (MySQL) App: Updated app to add server domain to db logs to allow using same db with multiple instances.\n- Query Logs (SQL Server) App: Updated app to add server domain to db logs to allow using same db with multiple instances.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.4\nRelease Date: 26 January 2025\n\n- 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.\n- Added support for reading minute stats for given custom date time range (for max 2 hours range difference).\n- Added HTTP API and GUI option to export Query Logs as a CSV file.\n- Drop Requests App: Fixed bug that caused matching all requests when unknown record type was configured.\n- 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).\n- Query Logs (SQL Server) App: Added new app that supports logging query logs to Microsoft SQL Server.\n- Query Logs (MySQL) App: Added new app that supports logging query logs to MySQL database server.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.3\nRelease Date: 21 December 2024\n\n- 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.\n- 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.\n- Added feature to include Subject Alternative Name (SAN) entry for DNS admin web service local unicast addresses in the self-signed certificate.\n- 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.\n- 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.\n- Fixed bug in reloading SSL/TLS certificate for DNS admin web service and DNS-over-HTTPS service.\n- Fixed issue with Catalog zone SOA request that caused zone transfer to fail with BIND.\n- Query Logs (Sqlite): Updated the app to support logging response RTT value.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.2.2\nRelease Date: 2 December 2024\n\n- Fixed bug that caused DNS response to include bogus records even when Checking Disabled (CD) is set to false in request.\n\n## Version 13.2.1\nRelease Date: 30 November 2024\n\n- Updated server to allow DNS-over-HTTPS service to read X-Real-IP header from reverse proxy that are allowed by the ACL.\n- 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.\n- Fixed issue with handling connection abort condition for DNS-over-QUIC.\n- Fixed issue with handling a wildcard query case for ENT subdomain names in local zones.\n- Fixed issue with Forwarding where CNAME was not being resolved separately when upstream returned SOA in response authority section.\n- Fixed issue in DNS Application assembly loading implementation that caused issue loading dependencies for some scenarios.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.2\nRelease Date: 16 November 2024\n\n- 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.\n- 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.\n\n## Version 13.1.1\nRelease Date: 9 November 2024\n\n- 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.\n- 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.\n- Fixed issue in DNS-over-TCP and DNS-over-TLS client caused due to some platforms not supporting TCP keep alive socket options.\n- 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.\n- Filter AAAA App: added new option to configuring default TTL value.\n- DNS Rebinding Protection App: added new option to configure bypass networks.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.1\nRelease Date: 19 October 2024\n\n- Added new option to add Secondary Root Zone directly.\n- Added new notify option for Catalog zones to specify separate name servers only for Catalog zone updates.\n- Added option to configure blocking answer's TTL value in Settings.\n- Added option to make the `X-Real-IP` header customizable for admin web service and for DNS-over-HTTP optional protocol.\n- Multiple other minor bug fixes and improvements.\n- Filter AAAA App: updated app to support option to explicitly specify filter domain names.\n\n## Version 13.0.2\nRelease Date: 28 September 2024\n\n- Fixed issue with DNS-over-TLS and DNS-over-TCP protocols that would cause the underlying connection to close if original request gets canceled.\n- Multiple other minor bug fixes and improvements.\n\n## Version 13.0.1\nRelease Date: 23 September 2024\n\n- Fixed issue in using proxy with forwarders that caused failure to use DNS-over-TOR with Cloudflare's hidden service.\n\n## Version 13.0\nRelease Date: 22 September 2024\n\n- 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.\n- 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.\n- 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.\n- 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.\n- Added support for concurrency in recursive resolver to allow querying more than one name server at a time to improve resolution performance.\n- 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.\n- 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.\n- 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.\n- Added support for Responsible Person (RP) record [RFC 1183](https://www.rfc-editor.org/rfc/rfc1183).\n- Added option to enable/disable Concurrent Forwarding feature so as to allow having sequential forwarding support.\n- The DNS Server now supports Network Access Control Lists for Recursion, Zone Transfer, and Dynamic Updates in both the GUI and HTTP API.\n- Changed the Unsupported NSEC3 Iteration Value implementation due to bug in previous implementation that caused failure to validate in some cases.\n- Improved brute force protection implementation for admin web service for IPv6 networks.\n- Added feature to write client subnet query rate limiting events to log file to allow tracking.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 12.2.1\nRelease Date: 15 June 2024\n\n- Fixed issue in DHCP server that caused failure to allocate lease due to hash code mismatch.\n- Fixed issue that may create empty zone files after the zone was deleted.\n\n## Version 12.2\nRelease Date: 15 June 2024\n\n- Added support for NAPTR record type.\n- Added Default Responsible Person option in Settings to use when adding Primary Zones.\n- Updated Serve Stale implementation to allow configuring Answer TTL, Reset TTL, and Max Wait Time options in Settings.\n- Updated SVCB/HTTPS record implementation to add support for automatic IP address hints.\n- 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).\n- Updated DNS Server's System Tray app on Windows with new context menu option to allow configuring Automatic Firewall entry feature.\n- Fixed issue with NSEC proof validation for wildcard empty non-terminal (ENT) cases.\n- Fixed issue with QNAME minimization implementation caused when NSEC3 unsupported iteration count event is encountered while resolving.\n- Added support for .p12 certificate file extension along with existing .pfx extension.\n- 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.\n- Query Logs (Sqlite) App: Fixed issue of failing to load the app on Alpine Linux.\n- Multiple other minor bug fixes and improvements.\n\n## Version 12.1\nRelease Date: 16 March 2024\n\n- 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.\n  - The mitigation now allows max 4 DNSKEY records with key tag collision.\n  - Limits cryptographic failures to max 16. \n  - 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.\n- 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.\n  - More than 8 NSEC3 hash calculation per response will cause suspension of the task.\n  - After 16 suspensions the the validation will stop for the response.\n- Fixed [Non-Responsive Delegation Attack](https://www.usenix.org/system/files/sec23fall-prepub-309-afek.pdf) (NRDelegation Attack) vulnerability [CVE-2022-3204].\n- Fixed [NXNSAttack](https://arxiv.org/abs/2005.09107) vulnerability [CVE-2020-12662].\n- Implemented NSEC3 iteration limit of 100. NSEC3 with iterations of more than 100 will be treated as No Proof.\n- Added EDNS Client Subnet (ECS) override feature to allow the DNS server to use the provided network subnet with ECS for all outbound requests.\n- Secondary zones now allow configuring Dynamic Updates permissions in Zone Options.\n- Import zone feature now supports option to overwrite SOA serial from SOA record being imported.\n- DNS Client now supports EDNS Client Subnet (ECS) option to allow testing ECS related issues with ease.\n- DNS cache entries now show request meta data to allow knowing the name server that provided the record data.\n- DHCP Scope now supports option to ignore Client Identifier option in requests to allow using the client's hardware address for lease management.\n- 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.\n- Advanced Forwarding App: Updated AdGuard upstream implementation to support multiple forwarders.\n- Geo Continent App: Updated app to support MaxMind ISP/ASN database to allow returning optimal ECS scope prefix in response.\n- Geo Country App: Updated app to support MaxMind ISP/ASN database to allow returning optimal ECS scope prefix in response.\n- Geo Distance App: Updated app to support MaxMind ISP/ASN database to allow returning optimal ECS scope prefix in response.\n- Fixed bug in authoritative zone wildcard matching.\n- Multiple other minor bug fixes and improvements.\n\n## Version 12.0.1\nRelease Date: 8 February 2024\n\n- Fixed bug in authoritative zone wildcard matching for empty non-terminal (ENT) records.\n- Fixed other minor issues.\n\n## Version 12.0\nRelease Date: 4 February 2024\n\n- 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.\n- 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.\n- 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.\n- Added transport protocol types chart on Dashboard which shows the protocol stats for the requests received by the DNS server.\n- Added feature to specify one or more source addresses for outbound DNS requests when the server is connected to two or more networks.\n- 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.\n- Added option to specify QPM bypass list to allow IP addresses or networks to bypass rate limiting restrictions.\n- 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.\n- Updated DNS-over-HTTPS implementation to work over SOCKS5 proxy when using HTTP/3 protocol (URL with `h3` scheme).\n- Added support for automatic initializing of DNS server root servers list with priming queries [RFC 8109](https://datatracker.ietf.org/doc/rfc8109/).\n- Conditional Forwarder Zones now support Dynamic Updates [RFC 2136](https://datatracker.ietf.org/doc/rfc2136/).\n- DNS Rebinding Protection App: A new app available that protects from DNS rebinding attacks using configured private domains and networks.\n- NX Domain Override App: New app to allow overriding NX Domain response to with custom A/AAAA record response for configured domain names.\n- Block Page App: Updated the app to use Kestrel web server and allow configuring multiple web servers that listen on different IP addresses.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.5.3\nRelease Date: 7 November 2023\n\n- Fixed bug in authoritative zone wildcard matching which caused NXDOMAIN response for some subdomain name requests.\n\n## Version 11.5.2\nRelease Date: 31 October 2023\n\n- Fixed bug in zone Dynamic Updates allowed IP/network addresses that caused failure to match with request IP address.\n\n## Version 11.5.1\nRelease Date: 30 October 2023\n\n- Fixed bug in validation code for DNS-over-TLS library that caused failure when trying to use the protocol.\n- Advanced Blocking App: Fixed minor issue in initializing the app.\n\n## Version 11.5\nRelease Date: 29 October 2023\n\n- Added support to import and export zones in standard RFC 1035 text file format.\n- Added feature to clone an existing zone with all its records and zone options.\n- Added DS Info viewer that shows all the info needed for updating DS records for the signed primary zone in a single view.\n- Added option to configure IP/network addresses that are allowed to perform zone transfer for all local zones without any TSIG authentication.\n- Added option to configure IP/network addresses that are allowed to bypass domain name blocking.\n- Added option to independently configure HTTP/3 protocol for DNS web service.\n- Added option to ignore resolver error logs so as to limit the log file size.\n- Added zone last modified date time stamp.\n- 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.\n- Updated DNS web service to revert to old local end point if new end point fails to bind.\n- Zone Options for zone transfer name servers and dynamic updates IP addresses can now accept network addresses too.\n- Updated conditional forwarder zones to allow bypassing default proxy configured in the DNS Server Settings.\n- 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.\n- 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.\n- Split Horizon App: Address translation now supports using network addresses too for external to internal translation.\n- Default Records App: New app added that allows setting one or more default records for configured local zones.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.4.1\nRelease Date: 13 August 2023\n\n- Fixed issue that caused backup operations to fail.\n- Fixed minor issue with incremental zone transfer which caused empty nodes to not get removed from secondary zones.\n\n## Version 11.4\nRelease Date: 12 August 2023\n\n- 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.\n- Updated TLS certificate implementation to allow the TLS handshake to always send the certificate chain.\n- Updated Backup and Restore feature to include Web Service and Optional Protocols certificate files when they exist within the DNS server's config folder.\n- Added DNS server uptime info in the About section.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.3\nRelease Date: 2 July 2023\n\n- Added support for URI record type ([RFC 7553](https://www.rfc-editor.org/rfc/rfc7553.html)).\n- Added support for `dohpath` parameter for SVCB record type ([draft-ietf-add-svcb-dns](https://datatracker.ietf.org/doc/draft-ietf-add-svcb-dns/)).\n- Added support for configuring generic parameter for SVCB & HTTPS record types in UI.\n- 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.\n- 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.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.2\nRelease Date: 27 May 2023\n\n- Added support for SVCB and HTTPS record types ([draft-ietf-dnsop-svcb-https](https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/)).\n- Added support for managing unknown (unsupported) record types.\n- Auto PTR App: Added new DNS app that can generate automatic responses for PTR requests.\n- Weighted Round Robin App: Added new app to allow returning responses with weighted round robin load balancing.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.1.1\nRelease Date: 1 May 2023\n\n- Fixed issue of UDP socket pool exhaustion on Windows platform causing all outbound UDP requests to fail.\n\n## Version 11.1\nRelease Date: 29 April 2023\n\n- Added support for Internationalized Domain Names (IDN).\n- Added support for primary zone's SOA record to have serial number date scheme.\n- 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.\n- Fixed bug in validation check during refreshing RRSIG records when primary zone is signed with NSEC3.\n- Fixed bug in NSEC3 record's types field which caused missing of RRSIG type entry.\n- Fixed issue to allow Kestrel web server to serve unknown file types to allow certbot webroot HTTP challenge to work as expected.\n- Advanced Forwarding App: Fixed the implementation to correctly store cached records per client subnet defined in the app's config. Added wildcard domain support.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.0.3\nRelease Date: 11 March 2023\n\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.0.2\nRelease Date: 26 February 2023\n\n- Fixed issue with DNS-over-HTTP private IP check that was causing 403 response when using with reverse proxy.\n- Fixed issue with zone record pagination caused when zone has no records.\n\n## Version 11.0.1\nRelease Date: 25 February 2023\n\n- Changed allow list implementation to handle them separately and show allow list count on Dashboard.\n- Fixed bug in conditional forwarder zone for root zone that caused the DNS server to return RCODE=ServerFailure.\n- Fixed issues with DNS server's App request query handling sequence to fix issues with Advanced Forwarding app.\n- Fixed issues with block list parser to detect in-line comments.\n- Fixed issue of \"URI too long\" in save DHCP scope action.\n- Updated Linux install script to use new install path in `/opt` and new config path `/etc/dns` for new installations.\n- Updated Docker container to use new volume path `/etc/dns` for config.\n- Updated Docker container to correctly handle container stop event to gracefully shutdown the DNS server.\n- Updated Docker container to include `libmsquic` to allow QUIC support.\n- Multiple other minor bug fixes and improvements.\n\n## Version 11.0\nRelease Date: 18 February 2023\n\n- 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.\n- Added support for Zone Transfer over QUIC (XFR-over-QUIC) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html).\n- 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.\n- 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.\n- Added support to save DNS cache data to disk on server shutdown and to reload it at startup.\n- 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.\n- Updated DNS server domain name blocking feature to support wildcard block lists file format and Adblock Plus file format.\n- 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.\n- Updated web panel Zones GUI to support pagination.\n- 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.\n- Advanced Forwarding App: Added new DNS app to support bulk conditional forwarder.\n- 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).\n- Added support for TFTP Server Address DHCP option (150).\n- Added support for Generic DHCP option to allow configuring option currently not supported by the DHCP server.\n- Removed support for non-standard DNS-over-HTTPS (JSON) protocol.\n- Removed Newtonsoft.Json dependency from the DNS server and all DNS apps.\n- Multiple other minor bug fixes and improvements.\n\n## Version 10.0.1\nRelease Date: 4 December 2022\n\n- Fixed multiple issues in EDNS Client Subnet (ECS) implementation.\n- Fixed issue with serialization when saving permission data when there are more than 255 zones.\n- Failover App: Fixed issue with idle connection for HTTP/HTTPS probes.\n- QueryLogs (Sqlite) App: Fixes issue of open db file on windows installations.\n- Multiple other minor bug fixes and improvements.\n\n## Version 10.0\nRelease Date: 26 November 2022\n\n- 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.\n- 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.\n- Added support for SSHFP [RFC 4255](https://www.rfc-editor.org/rfc/rfc4255.html) record type.\n- Implemented EDNS Client Subnet (ECS) [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871) support for recursive resolution and forwarding.\n- 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.\n- 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.\n- 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.\n- Updated DNS Apps framework with `IDnsPostProcessor` interface to allow manipulating outbound responses by DNS apps.\n- 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.\n- DNS64 App: Added new app to support DNS64 function [RFC 6147](https://www.rfc-editor.org/rfc/rfc6147) for use by IPv6 only clients.\n- Advanced Blocking App: Upgraded the app code to use less memory when same block lists are used across multiple groups.\n- 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).\n- 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.\n- Added support for Domain Search DHCP option [RFC 3397](https://www.rfc-editor.org/rfc/rfc3397)\n- Added support for CAPWAP Access Controller DHCP option [RFC 5417](https://www.rfc-editor.org/rfc/rfc5417.html).\n- Added DHCP Scope option to disable DNS updates.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 9.1\nRelease Date: 9 October 2022\n\n- 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.\n- Updated dashboard to display main chart using client's local time instead of server's local time.\n- Fixed bug that caused error while adding new secondary zone.\n- Multiple other minor bug fixes and improvements.\n\n## Version 9.0\nRelease Date: 24 September 2022\n\n- Added multi-user role based access support. This allows creating multiple users and multiple role based groups with permission based access controls.\n- Added support for non-expiring API tokens to use with automation scripts.\n- Added zone level permissions support to allow access only to selected users or group members.\n- User profile options available to update each user's session timeout values.\n- 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.\n- Updated Conditional Forwarder zones to support APP records to allow using DNS Apps in these zones.\n- Option added in Settings to stop block list URL automatic update.\n- 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.\n- DNS Apps now support automatic updates. The DNS server will check for updates and install them automatically every 24 hours.\n- Split Horizon App: Added feature to configure collection of networks to use with APP record data.\n- 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/).\n- Fixed minor issues in DNSSEC validation for DNAME responses and for wildcard NO DATA responses.\n- DHCP scopes now support updating DNS records in both Primary and Forwarder zones.\n- DHCP scopes now support blocking dynamic allocations to devices with locally administered MAC address.\n- Multiple other minor bug fixes and improvements.\n\n## Version 8.1.4\nRelease Date: 3 July 2022\n- Fixed issue in recursive resolution that caused DNSSEC validation to fail in cases when the name server responds with out-of-bailiwick records.\n- Updated recursive resolver to update addresses async for all NS records to improve performance.\n- Multiple other minor bug fixes and improvements.\n\n## Version 8.1.3\nRelease Date: 11 June 2022\n- Added OpenDNS DoH end points to DNS Client and Forwarder quick select list.\n- Fixed issue of missing digest type support check that could cause exception to be thrown causing failure to resolve the DNSSEC signed domain name.\n\n## Version 8.1.2\nRelease Date: 28 May 2022\n- Fixed issue in Primary zone add and update record IXFR history when RRSet TTL was updated.\n- Fixed issue in DNSSEC validation for MX and SRV records caused due to incorrect comparison of record data.\n- Fixed issue in SOA record responsible person parameter parsing.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 8.1.1\nRelease Date: 21 May 2022\n- Added Sync Failed and Notify Failed zone status to indicate issues between primary and secondary zones synchronization.\n- Added more options in zone options to configure zone transfer and notify settings.\n- Fixed DNSSEC signed primary zone key rollover timing issues as per [RFC 7583](https://datatracker.ietf.org/doc/html/rfc7583).\n- Fixed issue in recursive resolver by adding zone cut validation for glue records.\n- Multiple other minor bug fixes and improvements.\n\n## Version 8.1\nRelease Date: 8 May 2022\n- 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.\n- Added maximum cache entires option to limit memory usage by removing least recently used data from cache.\n- Implemented NS revalidation to revalidate parent side NS records when their TTL expires.\n- Updated the web console to store session token in local storage to prevent logging out on page reload.\n- DropRequests App: Added support to block entire zone for the configured QNAME.\n- Fixed bug in primary zone IXFR history caused due to missing SOA serial check.\n- Fixed issues with wrong IXFR history entries for DNSKEY records in primary zone.\n- Multiple other minor bug fixes and improvements.\n\n## Version 8.0.2\nRelease Date: 3 April 2022\n- Fixed bug in Conditional Forwarder zones that would cause ServerFailure responses for some queries.\n- Fixed issue of setting minimum TTL value to NSEC & NSEC3 records in Primary signed zones when SOA value is changed.\n- Fixed issue in parsing DNS-over-HTTPS JSON response for NSEC and NSEC3 records.\n- Multiple other minor bug fixes and improvements.\n\n## Version 8.0.1\nRelease Date: 29 March 2022\n- Fixed bug in Conditional Forwarder zones due to zone cut validation causing negative cache entry for CNAME responses which resulted in partial responses.\n- Fixed issue with handling FormatError response that were missing question section for EDNS requests.\n- Fixed minor issue with DNSSEC validation for unsigned zone when forwarder returns empty NXDOMAIN responses.\n- Fixed issue with NODATA response handling for ANAME records.\n- Fixed issue with record comment validation causing error when saving SOA records in zones.\n- Multiple other minor bug fixes and improvements.\n\n## Version 8.0\nRelease Date: 26 March 2022\n- Added EDNS support [RFC 6891](https://datatracker.ietf.org/doc/html/rfc6891).\n- Added Extended DNS Errors [RFC 8914](https://datatracker.ietf.org/doc/html/rfc8914).\n- Added DNSSEC validation support with RSA & ECDSA algorithms for recursive resolver, forwarders, and conditional forwarders.\n- Added DNSSEC support for all supported DNS transport protocols including encrypted DNS protocols (DoT, DoH, DoH JSON).\n- Added DNSSEC zone signing support with RSA & ECDSA algorithms.\n- Updated DNS Client to support DNSSEC validation.\n- Updated proprietary FWD record which is used with Conditional Forwarder Zones for DNSSEC validation and HTTP/SOCKS5 proxy support.\n- 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.\n- Upgraded codebase to .NET 6 runtime.\n- Query Logs App: Added wildcard search support for domain names.\n- Fixed multiple issues with DHCP server.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 7.1\nRelease Date: 23 October 2021\n- Added option in settings to automatically configure a self signed certificate for DNS web service.\n- 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.\n- Block Page App: Added support for automatic self signed certificate to allow showing block page for HTTPS websites.\n- Drop Requests App: Added option to drop malformed DNS requests.\n- 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.\n- Advanced Blocking App: Fixed bug in loading regex block list which caused the app to not block the domain names as expected.\n- Added logging in DNS server to know why a zone transfer request was refused by the server.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 7.0\nRelease Date: 2 October 2021\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- NX Domain App: This new app allows blocking domain names with a NXDOMAIN response.\n- 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.\n- Failover App: Implemented under maintenance feature to indicate if an address is taken down for maintenance.\n- Added Ping check option in DHCP scopes to allow detecting if an IP address is already in use before leasing it.\n- Added option to allow removing an allocated DHCP lease.\n- 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.\n- Multiple other minor bug fixes and improvements.\n\n## Version 6.4.1\nRelease Date: 21 August 2021\n- Implemented Delegation Revalidation [draft-ietf-dnsop-ns-revalidation-01](https://datatracker.ietf.org/doc/draft-ietf-dnsop-ns-revalidation/) in recursive resolver.\n- Fixed issues with DNS-over-TLS due to \"dot\" ALPN causing SSL handshake to fail when using NextDNS as forwarder.\n- 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.\n- Updated allowed list URL implementation to check for domains zone wise so that subdomain names from blocked list URLs too are allowed.\n- Updated DNS Failover App to v1.4 to fix implementation issues.\n- Multiple other minor bug fixes and improvements.\n\n## Version 6.4\nRelease Date: 14 August 2021\n- Added DNAME record [RFC 6672](https://datatracker.ietf.org/doc/html/rfc6672) support.\n- Implemented incremental zone transfer (IXFR) [RFC 1995](https://datatracker.ietf.org/doc/html/rfc1995) support.\n- Implemented secret key transaction authentication (TSIG) [RFC 8945](https://datatracker.ietf.org/doc/html/rfc8945) support for zone transfers.\n- 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.\n- Added advance options in Settings to control TTL values in Cache.\n- Added Resync button to force resync Secondary and Stub zones.\n- Updated query rate limiting feature to allow limiting requests from the client's subnet.\n- Updated SplitHorizon App to support configuring CIDR networks.\n- 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.\n- Fixed issues with log file rolling when using local time.\n- Multiple other minor bug fixes and improvements.\n- Updated few API calls which may cause issues in 3rd party clients if they are not updated before deploying this new version.\n\n## Version 6.3\nRelease Date: 6 June 2021\n\n- Added Failover App in DNS App Store.\n- Added comments option to DNS records in Zones.\n- Added Recursion ACL support to specify allowed and denied networks that can perform recursion.\n- Added Zone Options feature to allow configuring Zone Transfer and Notify settings per zone.\n- Added Queries Per Minute (QPM) Limit feature to limit the number of queries being made by an IP address.\n- Added feature to specify custom IP addresses for blocked domain names.\n- Added feature to temporarily/permanently disable blocking of domain names.\n- 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.\n- Fixed multiple issues in QNAME minimization implementation.\n- Fixed multiple DNS Client implementation issues.\n- Multiple other minor bug fixes and improvements.\n- Updated few API calls which may cause issues in 3rd party clients if they are not updated before deploying this new version.\n\n## Version 6.2.3\nRelease Date: 2 May 2021\n\n- Improved DNS Apps interface to show if updates are available in the installed apps list.\n- Updated stats module to truncate daily stats data to optimize memory usage.\n- Fixed issue with QNAME minimization caused due to missing check when response contained no answer and no authority.\n- Fixed issue in logger which would fail to start in certain conditions.\n- Updated DNS Apps to shuffle addresses in response to allow load balancing.\n\n## Version 6.2.2\nRelease Date: 24 April 2021\n\n- Fixed issues with recursive resolution.\n- Fixed issue in parsing AXFR response.\n- Fixed missing tags in responses to reflect correct stats on dashboard.\n- Fixed issue with web console redirection on saving settings when using a reverse proxy.\n- Multiple other minor bug fixes and improvements.\n\n## Version 6.2.1\nRelease Date: 17 April 2021\n\n- Updated DNS Cache serve stale implementation for better performance.\n- Implemented CNAME resolution optimization in DNS Cache and Auth Zone.\n- 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.\n- Fixed issue in DNS client caused when response greater than the buffer size is received.\n\n## Version 6.2\nRelease Date: 11 April 2021\n\n- Fixed critical bug in block list condition check causing server to respond with `RCODE=Refused` when only using Blocked zone.\n- Added option to respond with `RCODE=NxDomain` for blocked domains instead of returning `0.0.0.0` address.\n- 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.\n\n## Version 6.1\nRelease Date: 10 April 2021\n\n- Added DNS App Store feature that list all available apps for quick and easy installation and update.\n- Added 'Overwrite' option in Add Record for zones.\n- Multiple ANAME record support added.\n- Added block list allowed URL feature to prevent domain names from getting added to the block list zone.\n- Fixed bug in ZoneTree.\n- Fixed bugs in DNS Apps.\n- Split Default DNS App into 5 independent apps that are now available on the DNS App Store.\n- Fixed issues in DNS Cache and updated code for memory optimization.\n- Upgraded all library projects to .NET 5.\n- Multiple other minor bug fixes and improvements.\n\n## Version 6.0\nRelease Date: 13 March 2021\n\n- Updated entire DNS code base to .NET 5 with new Windows installer. This upgrade will improve overall performance on Windows installations.\n- 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.\n- 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.\n- Updated dashboard charts to save legend selection state.\n- Updated dashboard with Custom date selection option to display stats.\n- Added option to configure max stats days in settings.\n- Added option to enable/disable QNAME minimization.\n- Added delete existing files option in Restore settings.\n- Added support to store query stats data to allow DNS cache auto prefetch to refresh cache when DNS server restarts.\n- Updated TLS certificate implementation to allow using self signed certificates for web console, DoH, and DoT.\n- Added DHCP lease Reserve/Unreserve options to allow quickly reserving lease for clients.\n- Updated DHCP reserved lease option to allow overriding client's host name.\n- Fixed issues with DNS cache auto prefetch feature.\n- Fixed multiple issues in DNS cache.\n- Fixed multiple vulnerabilities causing DNS cache poisoning.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.6\nRelease Date: 2 January 2021\n\n- 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.\n- Updated DNS and DHCP listener code to use async IO to improve performance.\n- Added HTTPS support for web service that provides the web console access.\n- Added support to change the web service local addresses.\n- 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.\n- Added HTTP compression support in the main web service.\n- Added HTTP compression for downloading block lists.\n- Added option to clear and delete all dashboard stats and auto clean up old stats files from disk\n- Added option to delete all log files and auto clean up old log files from disk.\n- Added configurable option to disable logging, allow logging in local time, and to change log folder path.\n- Added option in settings to define the refresh interval for block lists with a manual option to force refresh all block lists.\n- 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.\n- Fixed multiple issues in DHCP server's DNS record management.\n- 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.\n- Fixed html encoding issue in web app.\n- Added option in web app to list top 1000 clients, top domains and top blocked domains.\n- DNS cache serve stale feature made configurable with default serve stale TTL set to 3 days instead of 7 days.\n- Fixed issue in recursive resolver to avoid querying root servers when one of the parent zone's name servers exists in DNS cache.\n- Breaking changes in the `getDnsSettings` and `setDnsSettings` API calls will require API clients to update the code before updating the DNS server.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.5\nRelease Date: 14 November 2020\n\n- Added option to specify bootfile name for PXE booting.\n- Implemented DHCP vendor specific information option.\n- Implemented strict enforcing of exclusion list.\n- Fixed bug in DNS initial server name that was caused due to invalid characters in the computer name.\n- Added support for additional record processing for SRV records and fixed issues for NS and MX records processing.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.4\nRelease Date: 18 October 2020\n\n- Implemented QNAME randomization feature [draft-vixie-dnsext-dns0x20](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00).\n- Fixed bug causing infinite loop in certain conditions when using UDP as transport.\n- Fixed bug in DNS cache querying which caused the server to make unneeded queries when performing recursive resolution.\n- Added Create PTR Zone option when adding A or AAAA records.\n- Fixed issues with DHCP scope selection when using relay agent.\n- Implemented changes to allow changing DHCP scope IP allocation from dynamic to reserved and vice versa.\n- Updated DHCP scope to allow specifying Next Server Address for use with TFTP for booting.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.3\nRelease Date: 26 September 2020\n\n- Fixed issues with DHCP server that caused it to not work correctly with relay agents.\n- Updated DHCP server to support multiple scopes to work on a single network interface allowing it to provide different options for groups of devices.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.2\nRelease Date: 6 September 2020\n\n- Added feature to allow using `certbot` to renew TLS certificates automatically when using DNS-over-HTTPS and DNS-over-TLS.\n- Fixed issue in DHCP server that caused thread to block by implementing async methods.\n- Fixed bug in DNS client that caused QTYPE mismatch due to QNAME minimization.\n- Fixed issues in DNS-over-HTTPS client related to retries and http error handling.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.1\nRelease Date: 29 August 2020\n\n- Implemented async IO to allow the DNS server handle much higher concurrent loads.\n- Implemented independent thread pools for DNS web service and recursive resolver.\n- Fixed bug in block list downloader that caused 0 byte file downloads.\n- Fixed bug in DHCP server in creating reverse zone.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.0.2\nRelease Date: 18 July 2020\n\n- Fixed issue of missing port for \"This Server\" in DNS Client.\n- Added domain name that was blocked in the TXT record.\n- Fixed bugs in CNAME cloaking implementation.\n- Upgraded .NET Framework version to v4.8.\n- Multiple other minor bug fixes and improvements.\n\n## Version 5.0.1\nRelease Date: 6 July 2020\n\n- Fixed serialization bug for TXT records.\n- Fixed issue with reading DnsDatagram for DoH POST requests.\n- Fixed bug in json serialization of DnsDatagram for DoH json format.\n- Fixed bug in RTT calculation for DoH json Connection.\n\n## Version 5.0\nRelease Date: 4 July 2020\n\n- DNS Server local end points support to allow specifying alternate ports for UDP and TCP protocols.\n- DNS Server performance issues caused by thread contention fixed.\n- CNAME cloaking implemented to block domain names that resolve to CNAME which are blocked.\n- 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.\n- QNAME minimization support in recursive resolver [draft-ietf-dnsop-rfc7816bis-04](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-rfc7816bis-04).\n- ANAME propriety record support to allow using CNAME like feature at zone root.\n- Added primary zones with NOTIFY implementation [RFC 1996](https://datatracker.ietf.org/doc/html/rfc1996).\n- Added secondary zones with NOTIFY implementation [RFC 1996](https://datatracker.ietf.org/doc/html/rfc1996).\n- Added stub zones with feature to override records.\n- Added conditional forwarder zones with all protocols including DNS-over-HTTPS and DNS-over-TLS support.\n- Conditional forwarder zones with feature to override records.\n- Conditional forwarder zones with support for multiple forwarders with different sub domain names.\n- ByteTree based zone tree implementation which is a complete lock-less and thread safe tree allowing concurrent read and write operations.\n- Fixed bug in parsing large TXT records.\n- DNS Client with internal support for concurrent querying. This allows querying multiple forwarders simultaneously to return fastest response of all.\n- DNS Client with support to import records via zone transfer.\n- Multiple other bug fixes in DNS and DHCP modules.\n"
  },
  {
    "path": "DnsServer.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.0.32014.148\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DnsServerApp\", \"DnsServerApp\\DnsServerApp.csproj\", \"{ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DnsServerWindowsService\", \"DnsServerWindowsService\\DnsServerWindowsService.csproj\", \"{7873B2B8-01BA-48BC-B4B0-0857FFD873C9}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DnsServerCore\", \"DnsServerCore\\DnsServerCore.csproj\", \"{4494B79B-588C-41F2-95AD-0897123AF154}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DnsServerSystemTrayApp\", \"DnsServerSystemTrayApp\\DnsServerSystemTrayApp.csproj\", \"{2F91BD07-2CEE-47FA-8486-457B54612B4C}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DnsServerCore.ApplicationCommon\", \"DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\", \"{4ABB6715-A66F-482F-BA13-88CDFD33B2C5}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Apps\", \"Apps\", \"{938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"GeoContinentApp\", \"Apps\\GeoContinentApp\\GeoContinentApp.csproj\", \"{39C1822D-061A-43D0-93BE-6F900CC688B6}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"GeoCountryApp\", \"Apps\\GeoCountryApp\\GeoCountryApp.csproj\", \"{338DDC94-6149-4A0E-A7A0-2630EA6BEA68}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"GeoDistanceApp\", \"Apps\\GeoDistanceApp\\GeoDistanceApp.csproj\", \"{1FE525BD-16BC-4F64-B31C-4E5AF70317A6}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"SplitHorizonApp\", \"Apps\\SplitHorizonApp\\SplitHorizonApp.csproj\", \"{CACE22C7-02A2-4579-BBC7-39F544CAD1A5}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"WhatIsMyDnsApp\", \"Apps\\WhatIsMyDnsApp\\WhatIsMyDnsApp.csproj\", \"{F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"FailoverApp\", \"Apps\\FailoverApp\\FailoverApp.csproj\", \"{099D27AF-3AEB-495A-A5D0-46DA59CC9213}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DropRequestsApp\", \"Apps\\DropRequestsApp\\DropRequestsApp.csproj\", \"{738079D1-FA5A-40CD-8A27-D831919EE209}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"QueryLogsSqliteApp\", \"Apps\\QueryLogsSqliteApp\\QueryLogsSqliteApp.csproj\", \"{186DEF23-863E-4954-BE16-5E5FCA75ECA2}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"LogExporterApp\", \"Apps\\LogExporterApp\\LogExporterApp.csproj\", \"{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"AdvancedBlockingApp\", \"Apps\\AdvancedBlockingApp\\AdvancedBlockingApp.csproj\", \"{A4C31093-CA65-42D4-928A-11907076C0DE}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NxDomainApp\", \"Apps\\NxDomainApp\\NxDomainApp.csproj\", \"{BB0010FC-20E9-4397-BF9B-C9955D9AD339}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"BlockPageApp\", \"Apps\\BlockPageApp\\BlockPageApp.csproj\", \"{45C6F9AD-57D6-4D6D-9498-10B5C828E47E}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"WildIpApp\", \"Apps\\WildIpApp\\WildIpApp.csproj\", \"{8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NoDataApp\", \"Apps\\NoDataApp\\NoDataApp.csproj\", \"{BE08D981-DDB0-4314-A571-D68EDF0F3971}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Dns64App\", \"Apps\\Dns64App\\Dns64App.csproj\", \"{3514C4B4-78C1-46A1-82D5-4E676DD114FA}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DnsBlockListApp\", \"Apps\\DnsBlockListApp\\DnsBlockListApp.csproj\", \"{9F2EC41F-6A9E-47C4-B47B-75190D5B6903}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"AdvancedForwardingApp\", \"Apps\\AdvancedForwardingApp\\AdvancedForwardingApp.csproj\", \"{42DD2C37-4082-4E33-9AB0-04A97290D5B7}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"AutoPtrApp\", \"Apps\\AutoPtrApp\\AutoPtrApp.csproj\", \"{06AB9E37-5532-4CDB-8D6C-D3575594CC7E}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"WeightedRoundRobinApp\", \"Apps\\WeightedRoundRobinApp\\WeightedRoundRobinApp.csproj\", \"{29688452-F88A-49F5-9C98-BE7B2812C522}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"ZoneAliasApp\", \"Apps\\ZoneAliasApp\\ZoneAliasApp.csproj\", \"{6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NxDomainOverrideApp\", \"Apps\\NxDomainOverrideApp\\NxDomainOverrideApp.csproj\", \"{44B057A5-3BF6-412F-8B86-D1C854CB3973}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DefaultRecordsApp\", \"Apps\\DefaultRecordsApp\\DefaultRecordsApp.csproj\", \"{BCA1D22A-058D-4817-8BFC-6125478C30BA}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DnsRebindingProtectionApp\", \"Apps\\DnsRebindingProtectionApp\\DnsRebindingProtectionApp.csproj\", \"{159014D9-662B-429E-8006-495A9B99B902}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"FilterAaaaApp\", \"Apps\\FilterAaaaApp\\FilterAaaaApp.csproj\", \"{0A9B7F39-80DA-4084-AD47-8707576927ED}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"QueryLogsSqlServerApp\", \"Apps\\QueryLogsSqlServerApp\\QueryLogsSqlServerApp.csproj\", \"{6F655C97-FD43-4FE1-B15A-6C783D2D91C9}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"QueryLogsMySqlApp\", \"Apps\\QueryLogsMySqlApp\\QueryLogsMySqlApp.csproj\", \"{699E2A1D-D917-4825-939E-65CDB2B16A96}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"MispConnectorApp\", \"Apps\\MispConnectorApp\\MispConnectorApp.csproj\", \"{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}\"\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"DnsServerCore.HttpApi\", \"DnsServerCore.HttpApi\\DnsServerCore.HttpApi.csproj\", \"{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{4494B79B-588C-41F2-95AD-0897123AF154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{4494B79B-588C-41F2-95AD-0897123AF154}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{4494B79B-588C-41F2-95AD-0897123AF154}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{4494B79B-588C-41F2-95AD-0897123AF154}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{2F91BD07-2CEE-47FA-8486-457B54612B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{2F91BD07-2CEE-47FA-8486-457B54612B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{2F91BD07-2CEE-47FA-8486-457B54612B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{2F91BD07-2CEE-47FA-8486-457B54612B4C}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{39C1822D-061A-43D0-93BE-6F900CC688B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{39C1822D-061A-43D0-93BE-6F900CC688B6}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{39C1822D-061A-43D0-93BE-6F900CC688B6}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{39C1822D-061A-43D0-93BE-6F900CC688B6}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{738079D1-FA5A-40CD-8A27-D831919EE209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{738079D1-FA5A-40CD-8A27-D831919EE209}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{738079D1-FA5A-40CD-8A27-D831919EE209}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{738079D1-FA5A-40CD-8A27-D831919EE209}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{A4C31093-CA65-42D4-928A-11907076C0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{A4C31093-CA65-42D4-928A-11907076C0DE}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{BE08D981-DDB0-4314-A571-D68EDF0F3971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{BE08D981-DDB0-4314-A571-D68EDF0F3971}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{BE08D981-DDB0-4314-A571-D68EDF0F3971}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{BE08D981-DDB0-4314-A571-D68EDF0F3971}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{29688452-F88A-49F5-9C98-BE7B2812C522}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{29688452-F88A-49F5-9C98-BE7B2812C522}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{29688452-F88A-49F5-9C98-BE7B2812C522}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{29688452-F88A-49F5-9C98-BE7B2812C522}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{44B057A5-3BF6-412F-8B86-D1C854CB3973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{44B057A5-3BF6-412F-8B86-D1C854CB3973}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{44B057A5-3BF6-412F-8B86-D1C854CB3973}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{44B057A5-3BF6-412F-8B86-D1C854CB3973}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{BCA1D22A-058D-4817-8BFC-6125478C30BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{BCA1D22A-058D-4817-8BFC-6125478C30BA}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{BCA1D22A-058D-4817-8BFC-6125478C30BA}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{BCA1D22A-058D-4817-8BFC-6125478C30BA}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{159014D9-662B-429E-8006-495A9B99B902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{159014D9-662B-429E-8006-495A9B99B902}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{159014D9-662B-429E-8006-495A9B99B902}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{159014D9-662B-429E-8006-495A9B99B902}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{0A9B7F39-80DA-4084-AD47-8707576927ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{0A9B7F39-80DA-4084-AD47-8707576927ED}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{0A9B7F39-80DA-4084-AD47-8707576927ED}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{0A9B7F39-80DA-4084-AD47-8707576927ED}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{699E2A1D-D917-4825-939E-65CDB2B16A96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{699E2A1D-D917-4825-939E-65CDB2B16A96}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{699E2A1D-D917-4825-939E-65CDB2B16A96}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{699E2A1D-D917-4825-939E-65CDB2B16A96}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(NestedProjects) = preSolution\n\t\t{39C1822D-061A-43D0-93BE-6F900CC688B6} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{338DDC94-6149-4A0E-A7A0-2630EA6BEA68} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{1FE525BD-16BC-4F64-B31C-4E5AF70317A6} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{CACE22C7-02A2-4579-BBC7-39F544CAD1A5} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{099D27AF-3AEB-495A-A5D0-46DA59CC9213} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{738079D1-FA5A-40CD-8A27-D831919EE209} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{186DEF23-863E-4954-BE16-5E5FCA75ECA2} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{A4C31093-CA65-42D4-928A-11907076C0DE} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{BB0010FC-20E9-4397-BF9B-C9955D9AD339} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{45C6F9AD-57D6-4D6D-9498-10B5C828E47E} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{8B6BEB00-0AC2-4680-A848-31AD8A0FCD82} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{BE08D981-DDB0-4314-A571-D68EDF0F3971} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{3514C4B4-78C1-46A1-82D5-4E676DD114FA} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{9F2EC41F-6A9E-47C4-B47B-75190D5B6903} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{42DD2C37-4082-4E33-9AB0-04A97290D5B7} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{06AB9E37-5532-4CDB-8D6C-D3575594CC7E} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{29688452-F88A-49F5-9C98-BE7B2812C522} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{6FF8C5F7-C98E-41C1-8FCD-25AEA0057673} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{44B057A5-3BF6-412F-8B86-D1C854CB3973} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{BCA1D22A-058D-4817-8BFC-6125478C30BA} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{159014D9-662B-429E-8006-495A9B99B902} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{0A9B7F39-80DA-4084-AD47-8707576927ED} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{6F655C97-FD43-4FE1-B15A-6C783D2D91C9} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{699E2A1D-D917-4825-939E-65CDB2B16A96} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\t\t{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {6747BB6D-2826-4356-A213-805FBCCF9201}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "DnsServerApp/DnsServerApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<OutputType>Exe</OutputType>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<Nullable>enable</Nullable>\n\t\t<ApplicationIcon>logo2.ico</ApplicationIcon>\n\t\t<Version>14.3</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<AssemblyName>DnsServerApp</AssemblyName>\n\t\t<RootNamespace>DnsServerApp</RootNamespace>\n\t\t<StartupObject>DnsServerApp.Program</StartupObject>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\DnsServerCore\\DnsServerCore.csproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Update=\"start.bat\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"start.sh\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"systemd.service\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t  <Folder Include=\"Properties\\PublishProfiles\\\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "DnsServerApp/Program.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.Runtime.InteropServices;\n\nnamespace DnsServerApp\n{\n    class Program\n    {\n        static async Task Main(string[] args)\n        {\n            bool throwIfBindFails = false;\n            string? configFolder = null;\n\n            foreach (string arg in args)\n            {\n                switch (arg)\n                {\n                    case \"--icu-test\":\n                        _ = System.Globalization.CultureInfo.CurrentCulture;\n                        return;\n\n                    case \"--stop-if-bind-fails\":\n                        throwIfBindFails = true;\n                        break;\n\n                    default:\n                        configFolder = arg;\n                        break;\n                }\n            }\n\n            ManualResetEvent waitHandle = new ManualResetEvent(false);\n            ManualResetEvent exitHandle = new ManualResetEvent(false);\n            DnsWebService? service = null;\n            PosixSignalRegistration? psr = null;\n\n            try\n            {\n                Uri updateCheckUri;\n\n                switch (Environment.OSVersion.Platform)\n                {\n                    case PlatformID.Win32NT:\n                        updateCheckUri = new Uri(\"https://go.technitium.com/?id=41\");\n                        break;\n\n                    default:\n                        updateCheckUri = new Uri(\"https://go.technitium.com/?id=42\");\n                        break;\n                }\n\n                service = new DnsWebService(configFolder, updateCheckUri);\n                await service.StartAsync(throwIfBindFails);\n\n                Console.CancelKeyPress += delegate (object? sender, ConsoleCancelEventArgs e)\n                {\n                    e.Cancel = true;\n                    waitHandle.Set();\n                };\n\n                AppDomain.CurrentDomain.ProcessExit += delegate (object? sender, EventArgs e)\n                {\n                    waitHandle.Set();\n                    exitHandle.WaitOne();\n                };\n\n                if (Environment.OSVersion.Platform == PlatformID.Unix)\n                {\n                    psr = PosixSignalRegistration.Create(PosixSignal.SIGTERM, delegate (PosixSignalContext context)\n                    {\n                        waitHandle.Set();\n                        exitHandle.WaitOne();\n                    });\n                }\n\n                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...\");\n\n                waitHandle.WaitOne();\n            }\n            catch (Exception ex)\n            {\n                Console.WriteLine(ex.ToString());\n            }\n            finally\n            {\n                Console.WriteLine(\"\\r\\nTechnitium DNS Server is stopping...\");\n\n                service?.Dispose();\n                psr?.Dispose();\n\n                Console.WriteLine(\"Technitium DNS Server was stopped successfully.\");\n                exitHandle.Set();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerApp/Properties/PublishProfiles/FolderProfile.pubxml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->\n<Project>\n  <PropertyGroup>\n    <Configuration>Release</Configuration>\n    <Platform>Any CPU</Platform>\n    <PublishDir>bin\\Release\\publish\\</PublishDir>\n    <PublishProtocol>FileSystem</PublishProtocol>\n    <_TargetId>Folder</_TargetId>\n    <TargetFramework>net9.0</TargetFramework>\n    <SelfContained>false</SelfContained>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "DnsServerApp/install.sh",
    "content": "#!/bin/sh\n\ndotnetDir=\"/opt/dotnet\"\ndotnetVersion=\"9.0\"\ndotnetRuntime=\"Microsoft.AspNetCore.App 9.0.\"\ndotnetUrl=\"https://dot.net/v1/dotnet-install.sh\"\n\nif [ -d \"/etc/dns/config\" ]\nthen\n    dnsDir=\"/etc/dns\"\nelse\n    dnsDir=\"/opt/technitium/dns\"\nfi\n\ndnsConfig=\"/etc/dns\"\ndnsTar=\"$dnsDir/DnsServerPortable.tar.gz\"\ndnsUrl=\"https://download.technitium.com/dns/DnsServerPortable.tar.gz\"\n\ninstallLog=\"$dnsDir/install.log\"\n\necho \"\"\necho \"===============================\"\necho \"Technitium DNS Server Installer\"\necho \"===============================\"\necho \"\"\n\nmkdir -p $dnsDir\necho \"\" > $installLog\n\nif dotnet --list-runtimes 2> /dev/null | grep -q \"$dotnetRuntime\"; \nthen\n    dotnetFound=\"yes\"\nelse\n    dotnetFound=\"no\"\nfi\n\nif [ ! -d $dotnetDir ] && [ \"$dotnetFound\" = \"yes\" ]\nthen\n    echo \"ASP.NET Core Runtime is already installed.\"\nelse\n    if [ -d $dotnetDir ] && [ \"$dotnetFound\" = \"yes\" ]\n    then\n        dotnetUpdate=\"yes\"\n        echo \"Updating ASP.NET Core Runtime...\"\n    else\n        dotnetUpdate=\"no\"\n        echo \"Installing ASP.NET Core Runtime...\"\n    fi\n\n    curl -sSL $dotnetUrl | bash /dev/stdin -c $dotnetVersion --runtime aspnetcore --no-path --install-dir $dotnetDir --verbose >> $installLog 2>&1\n\n    if [ ! -f \"/usr/bin/dotnet\" ]\n    then\n        ln -s $dotnetDir/dotnet /usr/bin >> $installLog 2>&1\n    fi\n\n    if dotnet --list-runtimes 2> /dev/null | grep -q \"$dotnetRuntime\"; \n    then\n        if [ \"$dotnetUpdate\" = \"yes\" ]\n        then\n            echo \"ASP.NET Core Runtime was updated successfully!\"\n        else\n            echo \"ASP.NET Core Runtime was installed successfully!\"\n        fi\n    else\n        echo \"Failed to install ASP.NET Core Runtime. Please check '$installLog' for details.\"\n        exit 1\n    fi\nfi\n\necho \"\"\necho \"Downloading Technitium DNS Server...\"\n\nif ! curl -o $dnsTar --fail $dnsUrl >> $installLog 2>&1\nthen\n    echo \"Failed to download Technitium DNS Server from: $dnsUrl\"\n    echo \"Please check '$installLog' for details.\"\n    exit 1\nfi\n\nif [ -d $dnsConfig ]\nthen\n    echo \"Updating Technitium DNS Server...\"\nelse\n    echo \"Installing Technitium DNS Server...\"\nfi\n\ntar -zxf $dnsTar -C $dnsDir >> $installLog 2>&1\n\necho \"\"\n\nif dotnet $dnsDir/DnsServerApp.dll --icu-test >> $installLog 2>&1\nthen\n    echo \"ICU package is already installed.\"\nelse\n    echo \"Checking for required ICU package...\"\n\n    if command -v apt-get >/dev/null 2>&1; then\n        # Debian/Ubuntu based\n        if ! dpkg -l | grep -q \"libicu\"; then\n            echo \"Installing required ICU package...\"\n            apt-get update >> $installLog 2>&1\n\n            # Try to install the most common package name\n            if apt-cache show libicu74 >/dev/null 2>&1; then\n                echo \"Installing libicu74 package...\"\n                apt-get install -y libicu74 >> $installLog 2>&1\n            elif apt-cache show libicu72 >/dev/null 2>&1; then\n                echo \"Installing libicu72 package...\"\n                apt-get install -y libicu72 >> $installLog 2>&1\n            elif apt-cache show libicu70 >/dev/null 2>&1; then\n                echo \"Installing libicu70 package...\"\n                apt-get install -y libicu70 >> $installLog 2>&1\n            else\n                # Fallback to a generic approach\n                echo \"No specific libicu package was found, trying generic installation...\"\n                apt-get install -y libicu* >> $installLog 2>&1\n            fi\n        fi\n    elif command -v dnf >/dev/null 2>&1; then\n        # Fedora/RHEL based\n        if ! rpm -qa | grep -q \"libicu\"; then\n            echo \"Installing required ICU package...\"\n            dnf install -y libicu >> $installLog 2>&1\n        fi\n    elif command -v yum >/dev/null 2>&1; then\n        # Older RHEL/CentOS systems\n        if ! rpm -qa | grep -q \"libicu\"; then\n            echo \"Installing required ICU package...\"\n            yum install -y libicu >> $installLog 2>&1\n        fi\n    elif command -v zypper >/dev/null 2>&1; then\n        # openSUSE based\n        if ! rpm -qa | grep -q \"libicu\"; then\n            echo \"Installing required ICU package...\"\n            zypper install -y libicu >> $installLog 2>&1\n        fi\n    elif command -v pacman >/dev/null 2>&1; then\n        # Arch based\n        if ! pacman -Q | grep -q \"icu\"; then\n            echo \"Installing required ICU package...\"\n            pacman -S --noconfirm icu >> $installLog 2>&1\n        fi\n    elif command -v apk >/dev/null 2>&1; then\n        # Alpine Linux\n        if ! apk list --installed | grep -q \"icu\"; then\n            echo \"Installing required ICU package...\"\n            apk add --no-cache icu >> $installLog 2>&1\n        fi\n    else\n        echo \"Failed to install Technitium DNS Server: could not determine package manager to install ICU package. Please install ICU package manually and try again.\"\n        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\"\n        exit 1\n    fi\n\n    #test again to confirm\n    if dotnet $dnsDir/DnsServerApp.dll --icu-test >> $installLog 2>&1\n    then\n        echo \"ICU package was installed successfully!\"\n    else\n        echo \"Failed to install Technitium DNS Server: failed to install ICU package. Please install ICU package manually and try again.\"\n        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\"\n        exit 1\n    fi\nfi\n\necho \"\"\n\nif ! [ \"$(ps --no-headers -o comm 1 | tr -d '\\n')\" = \"systemd\" ] \nthen\n    echo \"Failed to install Technitium DNS Server: systemd was not detected.\"\n    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\"\n    exit 1\nfi\n\nif [ -f \"/etc/systemd/system/dns.service\" ]\nthen\n    echo \"Restarting systemd service...\"\n    systemctl restart dns.service >> $installLog 2>&1\nelse\n    echo \"Configuring systemd service...\"\n    cp $dnsDir/systemd.service /etc/systemd/system/dns.service\n    systemctl enable dns.service >> $installLog 2>&1\n    \n    systemctl stop systemd-resolved >> $installLog 2>&1\n    systemctl disable systemd-resolved >> $installLog 2>&1\n    \n    systemctl start dns.service >> $installLog 2>&1\n    \n    rm /etc/resolv.conf >> $installLog 2>&1\n    echo -e \"# Generated by Technitium DNS Server Installer\\n\\nnameserver 127.0.0.1\" > /etc/resolv.conf 2>> $installLog\n    \n    if [ -f \"/etc/NetworkManager/NetworkManager.conf\" ]\n    then\n        echo -e \"[main]\\ndns=default\" >> /etc/NetworkManager/NetworkManager.conf 2>> $installLog\n    fi\nfi\n\necho \"\"\necho \"Technitium DNS Server was installed successfully!\"\necho \"Open http://$(cat /proc/sys/kernel/hostname):5380/ to access the web console.\"\necho \"\"\necho \"Donate! Make a contribution by becoming a Patron: https://www.patreon.com/technitium\"\necho \"\"\n"
  },
  {
    "path": "DnsServerApp/start.bat",
    "content": "dotnet DnsServerApp.dll"
  },
  {
    "path": "DnsServerApp/start.sh",
    "content": "#!/bin/sh\n\ndotnet DnsServerApp.dll\n"
  },
  {
    "path": "DnsServerApp/systemd.service",
    "content": "[Unit]\nDescription=Technitium DNS Server\n\n[Service]\nWorkingDirectory=/opt/technitium/dns\nExecStart=/usr/bin/dotnet /opt/technitium/dns/DnsServerApp.dll /etc/dns\nRestart=always\n# Restart service after 10 seconds if the dotnet service crashes:\nRestartSec=10\nSyslogIdentifier=dns-server\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "DnsServerApp/uninstall.sh",
    "content": "#!/bin/sh\n\ndotnetDir=\"/opt/dotnet\"\n\nif [ -d \"/etc/dns/config\" ]\nthen\n\tdnsDir=\"/etc/dns\"\nelse\n    dnsDir=\"/opt/technitium/dns\"\nfi\n\necho \"\"\necho \"=================================\"\necho \"Technitium DNS Server Uninstaller\"\necho \"=================================\"\necho \"\"\necho \"Uninstalling Technitium DNS Server...\"\n\nif [ -d $dnsDir ]\nthen\n\tif [ \"$(ps --no-headers -o comm 1 | tr -d '\\n')\" = \"systemd\" ] \n\tthen\n\t\tsudo systemctl disable dns.service >/dev/null 2>&1\n\t\tsudo systemctl stop dns.service >/dev/null 2>&1\n\t\trm /etc/systemd/system/dns.service >/dev/null 2>&1\n\t\t\n\t\trm /etc/resolv.conf >/dev/null 2>&1\n\t\techo \"nameserver 8.8.8.8\" >> /etc/resolv.conf\n\t\techo \"nameserver 1.1.1.1\" >> /etc/resolv.conf\n\tfi\n\n\trm -rf $dnsDir >/dev/null 2>&1\n\n\tif [ -d $dotnetDir ]\n\tthen\n\t\techo \"Uninstalling .NET Runtime...\"\n\t\trm /usr/bin/dotnet >/dev/null 2>&1\n\t\trm -rf $dotnetDir >/dev/null 2>&1\n\tfi\nfi\n\necho \"\"\necho \"Thank you for using Technitium DNS Server!\"\n"
  },
  {
    "path": "DnsServerCore/Auth/AuthManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Security.OTP;\n\nnamespace DnsServerCore.Auth\n{\n    sealed class AuthManager : IDisposable\n    {\n        #region variables\n\n        ConcurrentDictionary<string, Group> _groups = new ConcurrentDictionary<string, Group>(1, 4);\n        ConcurrentDictionary<string, User> _users = new ConcurrentDictionary<string, User>(1, 4);\n        ConcurrentDictionary<PermissionSection, Permission> _permissions = new ConcurrentDictionary<PermissionSection, Permission>(1, 11);\n        ConcurrentDictionary<string, UserSession> _sessions = new ConcurrentDictionary<string, UserSession>(1, 10);\n\n        readonly ConcurrentDictionary<IPAddress, int> _failedLoginAttemptNetworks = new ConcurrentDictionary<IPAddress, int>(1, 10);\n        const int MAX_LOGIN_ATTEMPTS = 5;\n\n        readonly ConcurrentDictionary<IPAddress, DateTime> _blockedNetworks = new ConcurrentDictionary<IPAddress, DateTime>(1, 10);\n        const int BLOCK_NETWORK_INTERVAL = 5 * 60 * 1000;\n\n        readonly string _configFolder;\n        readonly LogManager _log;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        #endregion\n\n        #region constructor\n\n        public AuthManager(string configFolder, LogManager log)\n        {\n            _configFolder = configFolder;\n            _log = log;\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveConfigFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n\n            LoadConfigFile();\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                //always save config here to write user login timestamps details\n                try\n                {\n                    SaveConfigFileInternal();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n                finally\n                {\n                    _pendingSave = false;\n                }\n            }\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region config\n\n        private void LoadConfigFile()\n        {\n            string configFile = Path.Combine(_configFolder, \"auth.config\");\n\n            try\n            {\n                bool passwordResetOption = false;\n\n                if (!File.Exists(configFile))\n                {\n                    string passwordResetConfigFile = Path.Combine(_configFolder, \"resetadmin.config\");\n\n                    if (File.Exists(passwordResetConfigFile))\n                    {\n                        passwordResetOption = true;\n                        configFile = passwordResetConfigFile;\n                    }\n                }\n\n                using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS, false);\n                }\n\n                _log.Write(\"DNS Server auth config file was loaded: \" + configFile);\n\n                if (passwordResetOption)\n                {\n                    User adminUser = GetUser(\"admin\");\n                    if (adminUser is null)\n                    {\n                        adminUser = CreateUser(\"Administrator\", \"admin\", \"admin\");\n                    }\n                    else\n                    {\n                        adminUser.ChangePassword(\"admin\");\n                        adminUser.Disabled = false;\n\n                        if (adminUser.TOTPEnabled)\n                            adminUser.DisableTOTP();\n                    }\n\n                    adminUser.AddToGroup(GetGroup(Group.ADMINISTRATORS));\n\n                    _log.Write(\"DNS Server has reset the password for user: admin\");\n                    SaveConfigFileInternal();\n\n                    try\n                    {\n                        File.Delete(configFile);\n                    }\n                    catch\n                    { }\n                }\n            }\n            catch (FileNotFoundException)\n            {\n                CreateDefaultConfig();\n\n                SaveConfigFileInternal();\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DNS Server encountered an error while loading auth config file: \" + configFile + \"\\r\\n\" + ex.ToString());\n                _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.\");\n                throw;\n            }\n        }\n\n        public void LoadOldConfig(string password, bool isPasswordHash)\n        {\n            User user = GetUser(\"admin\");\n            if (user is null)\n                user = CreateUser(\"Administrator\", \"admin\", \"admin\");\n\n            user.AddToGroup(GetGroup(Group.ADMINISTRATORS));\n\n            if (isPasswordHash)\n                user.LoadOldSchemeCredentials(password);\n            else\n                user.ChangePassword(password);\n\n            lock (_saveLock)\n            {\n                SaveConfigFileInternal();\n            }\n        }\n\n        public void LoadConfig(Stream s, bool isConfigTransfer, UserSession implantSession = null)\n        {\n            lock (_saveLock)\n            {\n                ReadConfigFrom(s, isConfigTransfer);\n\n                if (!isConfigTransfer)\n                {\n                    if (implantSession is not null)\n                    {\n                        //implant current user and session into config while restoring backup config\n                        using (MemoryStream mS = new MemoryStream())\n                        {\n                            //implant current user\n                            implantSession.User.WriteTo(new BinaryWriter(mS));\n\n                            mS.Position = 0;\n                            User newUser = new User(new BinaryReader(mS), _groups);\n                            newUser.AddToGroup(GetGroup(Group.ADMINISTRATORS));\n                            _users[newUser.Username] = newUser;\n\n                            //implant current session\n                            mS.SetLength(0);\n                            implantSession.WriteTo(new BinaryWriter(mS));\n\n                            mS.Position = 0;\n                            UserSession newSession = new UserSession(new BinaryReader(mS), _users);\n                            _sessions[newSession.Token] = newSession;\n                        }\n                    }\n                }\n\n                //save config file\n                SaveConfigFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void SaveConfigFileInternal()\n        {\n            string configFile = Path.Combine(_configFolder, \"auth.config\");\n\n            using (MemoryStream mS = new MemoryStream())\n            {\n                //serialize config\n                WriteConfigTo(mS);\n\n                //write config\n                mS.Position = 0;\n\n                using (FileStream fS = new FileStream(configFile, FileMode.Create, FileAccess.Write))\n                {\n                    mS.CopyTo(fS);\n                }\n            }\n\n            _log.Write(\"DNS Server auth config file was saved: \" + configFile);\n        }\n\n        public void SaveConfigFile()\n        {\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void ReadConfigFrom(Stream s, bool isConfigTransfer)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"AS\") //format\n                throw new InvalidDataException(\"DNS Server auth config file format is invalid.\");\n\n            ConcurrentDictionary<string, Group> groups = new ConcurrentDictionary<string, Group>(1, 4);\n            ConcurrentDictionary<string, User> users = new ConcurrentDictionary<string, User>(1, 4);\n            ConcurrentDictionary<PermissionSection, Permission> permissions = new ConcurrentDictionary<PermissionSection, Permission>(1, 11);\n            ConcurrentDictionary<string, UserSession> sessions = new ConcurrentDictionary<string, UserSession>(1, 10);\n\n            int version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    {\n                        int count = bR.ReadByte();\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            Group group = new Group(bR);\n                            groups.TryAdd(group.Name.ToLowerInvariant(), group);\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadByte();\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            User user = new User(bR, groups);\n                            users.TryAdd(user.Username, user);\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadInt32();\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            Permission permission = new Permission(bR, users, groups);\n                            permissions.TryAdd(permission.Section, permission);\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadInt32();\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            UserSession session = new UserSession(bR, users);\n                            if (!session.HasExpired())\n                                sessions.TryAdd(session.Token, session);\n                        }\n                    }\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"DNS Server auth config version not supported.\");\n            }\n\n            _groups = groups;\n            _users = users;\n\n            if (isConfigTransfer)\n            {\n                //sync only required permissions from newly loaded config\n                foreach (KeyValuePair<PermissionSection, Permission> permission in permissions)\n                {\n                    switch (permission.Key)\n                    {\n                        case PermissionSection.Zones:\n                            //sync user and group permissions as-is for zones section\n                            Permission zonesPermission = _permissions[PermissionSection.Zones];\n\n                            zonesPermission.SyncPermissions(permission.Value.UserPermissions);\n                            zonesPermission.SyncPermissions(permission.Value.GroupPermissions);\n                            break;\n\n                        default:\n                            _permissions[permission.Key] = permission.Value;\n                            break;\n                    }\n                }\n\n                //update all user objects in existing sessions to reflect the newly loaded config\n                foreach (KeyValuePair<string, UserSession> session in _sessions)\n                    session.Value.UpdateUserObject(_users);\n\n                //sync only API sessions from newly loaded config\n                foreach (KeyValuePair<string, UserSession> existingSession in _sessions)\n                {\n                    if (existingSession.Value.Type == UserSessionType.ApiToken)\n                    {\n                        if (!sessions.ContainsKey(existingSession.Key))\n                            _sessions.TryRemove(existingSession);\n                    }\n                }\n\n                foreach (KeyValuePair<string, UserSession> session in sessions)\n                {\n                    if (session.Value.Type == UserSessionType.ApiToken)\n                        _sessions[session.Key] = session.Value;\n                }\n            }\n            else\n            {\n                _permissions = permissions;\n                _sessions = sessions;\n            }\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"AS\")); //format\n            bW.Write((byte)1); //version\n\n            bW.Write(Convert.ToByte(_groups.Count));\n\n            foreach (KeyValuePair<string, Group> group in _groups)\n                group.Value.WriteTo(bW);\n\n            bW.Write(Convert.ToByte(_users.Count));\n\n            foreach (KeyValuePair<string, User> user in _users)\n                user.Value.WriteTo(bW);\n\n            bW.Write(_permissions.Count);\n\n            foreach (KeyValuePair<PermissionSection, Permission> permission in _permissions)\n                permission.Value.WriteTo(bW);\n\n            List<UserSession> activeSessions = new List<UserSession>(_sessions.Count);\n\n            foreach (KeyValuePair<string, UserSession> session in _sessions)\n            {\n                if (session.Value.HasExpired())\n                    _sessions.TryRemove(session.Key, out _);\n                else\n                    activeSessions.Add(session.Value);\n            }\n\n            bW.Write(activeSessions.Count);\n\n            foreach (UserSession session in activeSessions)\n                session.WriteTo(bW);\n        }\n\n        #endregion\n\n        #region private\n\n        private void CreateDefaultConfig()\n        {\n            Group adminGroup = CreateGroup(Group.ADMINISTRATORS, \"Super administrators\");\n            Group dnsAdminGroup = CreateGroup(Group.DNS_ADMINISTRATORS, \"DNS service administrators\");\n            Group dhcpAdminGroup = CreateGroup(Group.DHCP_ADMINISTRATORS, \"DHCP service administrators\");\n            Group everyoneGroup = CreateGroup(Group.EVERYONE, \"All users\");\n\n            SetPermission(PermissionSection.Dashboard, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Zones, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Cache, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Allowed, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Blocked, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Apps, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.DnsClient, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Settings, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.DhcpServer, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Administration, adminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Logs, adminGroup, PermissionFlag.ViewModifyDelete);\n\n            SetPermission(PermissionSection.Zones, dnsAdminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Cache, dnsAdminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Allowed, dnsAdminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Blocked, dnsAdminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Apps, dnsAdminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.DnsClient, dnsAdminGroup, PermissionFlag.ViewModifyDelete);\n            SetPermission(PermissionSection.Settings, dnsAdminGroup, PermissionFlag.ViewModifyDelete);\n\n            SetPermission(PermissionSection.DhcpServer, dhcpAdminGroup, PermissionFlag.ViewModifyDelete);\n\n            SetPermission(PermissionSection.Dashboard, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.Zones, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.Cache, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.Allowed, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.Blocked, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.Apps, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.DnsClient, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.DhcpServer, everyoneGroup, PermissionFlag.View);\n            SetPermission(PermissionSection.Logs, everyoneGroup, PermissionFlag.View);\n\n            string adminPassword = Environment.GetEnvironmentVariable(\"DNS_SERVER_ADMIN_PASSWORD\");\n            string adminPasswordFile = Environment.GetEnvironmentVariable(\"DNS_SERVER_ADMIN_PASSWORD_FILE\");\n\n            User adminUser;\n\n            if (!string.IsNullOrEmpty(adminPassword))\n            {\n                adminUser = CreateUser(\"Administrator\", \"admin\", adminPassword);\n            }\n            else if (!string.IsNullOrEmpty(adminPasswordFile))\n            {\n                try\n                {\n                    using (StreamReader sR = new StreamReader(adminPasswordFile, true))\n                    {\n                        string password = sR.ReadLine();\n                        adminUser = CreateUser(\"Administrator\", \"admin\", password);\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n\n                    adminUser = CreateUser(\"Administrator\", \"admin\", \"admin\");\n                }\n            }\n            else\n            {\n                adminUser = CreateUser(\"Administrator\", \"admin\", \"admin\");\n            }\n\n            adminUser.AddToGroup(adminGroup);\n        }\n\n        private async Task<User> AuthenticateUserAsync(string username, string password, string totp, IPAddress remoteAddress)\n        {\n            IPAddress network = GetClientNetwork(remoteAddress);\n\n            if (IsNetworkBlocked(network))\n                throw new DnsWebServiceException(\"Max limit of \" + MAX_LOGIN_ATTEMPTS + \" attempts exceeded. Access blocked for \" + (BLOCK_NETWORK_INTERVAL / 1000) + \" seconds.\");\n\n            User user = GetUser(username);\n\n            if ((user is null) || !user.PasswordHash.Equals(user.GetPasswordHashFor(password), StringComparison.Ordinal))\n            {\n                if (password != \"admin\")\n                {\n                    MarkFailedLoginAttempt(network);\n\n                    if (HasLoginAttemptExceedLimit(network, MAX_LOGIN_ATTEMPTS))\n                        BlockNetwork(network, BLOCK_NETWORK_INTERVAL);\n                }\n\n                await Task.Delay(1000);\n\n                throw new DnsWebServiceException(\"Invalid username or password for user: \" + username);\n            }\n\n            if (user.TOTPEnabled)\n            {\n                if (string.IsNullOrEmpty(totp))\n                    throw new TwoFactorAuthRequiredWebServiceException(\"A time-based one-time password (TOTP) is required for user: \" + username);\n\n                Authenticator authenticator = new Authenticator(user.TOTPKeyUri);\n\n                if (!authenticator.IsTOTPValid(totp))\n                {\n                    MarkFailedLoginAttempt(network);\n\n                    if (HasLoginAttemptExceedLimit(network, MAX_LOGIN_ATTEMPTS))\n                        BlockNetwork(network, BLOCK_NETWORK_INTERVAL);\n\n                    await Task.Delay(1000);\n\n                    throw new DnsWebServiceException(\"Invalid time-based one-time password (TOTP) was attempted for user: \" + username);\n                }\n            }\n\n            ResetFailedLoginAttempts(network);\n\n            if (user.Disabled)\n                throw new DnsWebServiceException(\"User account is disabled. Please contact your administrator.\");\n\n            return user;\n        }\n\n        private static IPAddress GetClientNetwork(IPAddress address)\n        {\n            switch (address.AddressFamily)\n            {\n                case AddressFamily.InterNetwork:\n                    return address.GetNetworkAddress(32);\n\n                case AddressFamily.InterNetworkV6:\n                    return address.GetNetworkAddress(64);\n\n                default:\n                    throw new InvalidOperationException();\n            }\n        }\n\n        private void MarkFailedLoginAttempt(IPAddress network)\n        {\n            _failedLoginAttemptNetworks.AddOrUpdate(network, 1, delegate (IPAddress key, int attempts)\n            {\n                return attempts + 1;\n            });\n        }\n\n        private bool HasLoginAttemptExceedLimit(IPAddress network, int limit)\n        {\n            if (!_failedLoginAttemptNetworks.TryGetValue(network, out int attempts))\n                return false;\n\n            return attempts >= limit;\n        }\n\n        private void ResetFailedLoginAttempts(IPAddress network)\n        {\n            _failedLoginAttemptNetworks.TryRemove(network, out _);\n        }\n\n        private void BlockNetwork(IPAddress network, int interval)\n        {\n            _blockedNetworks.TryAdd(network, DateTime.UtcNow.AddMilliseconds(interval));\n        }\n\n        private bool IsNetworkBlocked(IPAddress network)\n        {\n            if (!_blockedNetworks.TryGetValue(network, out DateTime expiry))\n                return false;\n\n            if (expiry > DateTime.UtcNow)\n            {\n                return true;\n            }\n            else\n            {\n                UnblockNetwork(network);\n                ResetFailedLoginAttempts(network);\n\n                return false;\n            }\n        }\n\n        private void UnblockNetwork(IPAddress network)\n        {\n            _blockedNetworks.TryRemove(network, out _);\n        }\n\n        #endregion\n\n        #region public\n\n        public User GetUser(string username)\n        {\n            if (_users.TryGetValue(username.ToLowerInvariant(), out User user))\n                return user;\n\n            return null;\n        }\n\n        public User CreateUser(string displayName, string username, string password, int iterations = User.DEFAULT_ITERATIONS)\n        {\n            if (_users.Count >= byte.MaxValue)\n                throw new DnsWebServiceException(\"Cannot create more than 255 users.\");\n\n            username = username.ToLowerInvariant();\n\n            User user = new User(displayName, username, password, iterations);\n\n            if (_users.TryAdd(username, user))\n            {\n                user.AddToGroup(GetGroup(Group.EVERYONE));\n                return user;\n            }\n\n            throw new DnsWebServiceException(\"User already exists: \" + username);\n        }\n\n        public void ChangeUsername(User user, string newUsername)\n        {\n            if (user.Username.Equals(newUsername, StringComparison.OrdinalIgnoreCase))\n                return;\n\n            string oldUsername = user.Username;\n            user.Username = newUsername;\n\n            if (!_users.TryAdd(user.Username, user))\n            {\n                user.Username = oldUsername; //revert\n                throw new DnsWebServiceException(\"User already exists: \" + newUsername);\n            }\n\n            _users.TryRemove(oldUsername, out _);\n        }\n\n        public async Task<User> ChangePasswordAsync(string username, string password, string totp, IPAddress remoteAddress, string newPassword, int iterations)\n        {\n            User user = await AuthenticateUserAsync(username, password, totp, remoteAddress);\n\n            user.ChangePassword(newPassword, iterations);\n\n            return user;\n        }\n\n        public bool DeleteUser(string username)\n        {\n            if (_users.TryRemove(username.ToLowerInvariant(), out User deletedUser))\n            {\n                //delete all sessions\n                foreach (UserSession session in GetSessions(deletedUser))\n                    DeleteSession(session.Token);\n\n                //delete all permissions\n                foreach (KeyValuePair<PermissionSection, Permission> permission in _permissions)\n                {\n                    permission.Value.RemovePermission(deletedUser);\n                    permission.Value.RemoveAllSubItemPermissions(deletedUser);\n                }\n\n                return true;\n            }\n\n            return false;\n        }\n\n        public Group GetGroup(string name)\n        {\n            if (_groups.TryGetValue(name.ToLowerInvariant(), out Group group))\n                return group;\n\n            return null;\n        }\n\n        public List<User> GetGroupMembers(Group group)\n        {\n            List<User> members = new List<User>();\n\n            foreach (KeyValuePair<string, User> user in _users)\n            {\n                if (user.Value.IsMemberOfGroup(group))\n                    members.Add(user.Value);\n            }\n\n            return members;\n        }\n\n        public void SyncGroupMembers(Group group, IReadOnlyDictionary<string, User> users)\n        {\n            //remove\n            foreach (KeyValuePair<string, User> user in _users)\n            {\n                if (!users.ContainsKey(user.Key))\n                    user.Value.RemoveFromGroup(group);\n            }\n\n            //set\n            foreach (KeyValuePair<string, User> user in users)\n                user.Value.AddToGroup(group);\n        }\n\n        public Group CreateGroup(string name, string description)\n        {\n            if (_groups.Count >= byte.MaxValue)\n                throw new DnsWebServiceException(\"Cannot create more than 255 groups.\");\n\n            Group group = new Group(name, description);\n\n            if (_groups.TryAdd(name.ToLowerInvariant(), group))\n                return group;\n\n            throw new DnsWebServiceException(\"Group already exists: \" + name);\n        }\n\n        public void RenameGroup(Group group, string newGroupName)\n        {\n            if (group.Name.Equals(newGroupName, StringComparison.OrdinalIgnoreCase))\n            {\n                group.Name = newGroupName;\n                return;\n            }\n\n            string oldGroupName = group.Name;\n            group.Name = newGroupName;\n\n            if (!_groups.TryAdd(group.Name.ToLowerInvariant(), group))\n            {\n                group.Name = oldGroupName; //revert\n                throw new DnsWebServiceException(\"Group already exists: \" + newGroupName);\n            }\n\n            _groups.TryRemove(oldGroupName.ToLowerInvariant(), out _);\n\n            //update users\n            foreach (KeyValuePair<string, User> user in _users)\n                user.Value.RenameGroup(oldGroupName);\n        }\n\n        public bool DeleteGroup(string name)\n        {\n            name = name.ToLowerInvariant();\n\n            switch (name)\n            {\n                case \"everyone\":\n                case \"administrators\":\n                case \"dns administrators\":\n                case \"dhcp administrators\":\n                    throw new InvalidOperationException(\"Access was denied.\");\n\n                default:\n                    if (_groups.TryRemove(name, out Group deletedGroup))\n                    {\n                        //remove all users from deleted group\n                        foreach (KeyValuePair<string, User> user in _users)\n                            user.Value.RemoveFromGroup(deletedGroup);\n\n                        //delete all permissions\n                        foreach (KeyValuePair<PermissionSection, Permission> permission in _permissions)\n                        {\n                            permission.Value.RemovePermission(deletedGroup);\n                            permission.Value.RemoveAllSubItemPermissions(deletedGroup);\n                        }\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public UserSession GetSession(string token)\n        {\n            if (_sessions.TryGetValue(token, out UserSession session))\n                return session;\n\n            return null;\n        }\n\n        public List<UserSession> GetSessions(User user)\n        {\n            List<UserSession> userSessions = new List<UserSession>();\n\n            foreach (KeyValuePair<string, UserSession> session in _sessions)\n            {\n                if (session.Value.User.Equals(user) && !session.Value.HasExpired())\n                    userSessions.Add(session.Value);\n            }\n\n            return userSessions;\n        }\n\n        public async Task<UserSession> CreateSessionAsync(UserSessionType type, string tokenName, string username, string password, string totp, IPAddress remoteAddress, string userAgent)\n        {\n            User user = await AuthenticateUserAsync(username, password, totp, remoteAddress);\n\n            UserSession session = new UserSession(type, tokenName, user, remoteAddress, userAgent);\n\n            if (!_sessions.TryAdd(session.Token, session))\n                throw new DnsWebServiceException(\"Error while creating session. Please try again.\");\n\n            user.LoggedInFrom(remoteAddress);\n\n            return session;\n        }\n\n        public UserSession CreateApiToken(string tokenName, string username, IPAddress remoteAddress, string userAgent)\n        {\n            User user = GetUser(username);\n            if (user is null)\n                throw new DnsWebServiceException(\"No such user exists: \" + username);\n\n            if (user.Disabled)\n                throw new DnsWebServiceException(\"Account is suspended.\");\n\n            UserSession session = new UserSession(UserSessionType.ApiToken, tokenName, user, remoteAddress, userAgent);\n\n            if (!_sessions.TryAdd(session.Token, session))\n                throw new DnsWebServiceException(\"Error while creating session. Please try again.\");\n\n            user.LoggedInFrom(remoteAddress);\n\n            return session;\n        }\n\n        public UserSession DeleteSession(string token)\n        {\n            if (_sessions.TryRemove(token, out UserSession session))\n                return session;\n\n            return null;\n        }\n\n        public Permission GetPermission(PermissionSection section)\n        {\n            if (_permissions.TryGetValue(section, out Permission permission))\n                return permission;\n\n            return null;\n        }\n\n        public Permission GetPermission(PermissionSection section, string subItemName)\n        {\n            if (_permissions.TryGetValue(section, out Permission permission))\n                return permission.GetSubItemPermission(subItemName);\n\n            return null;\n        }\n\n        public void SetPermission(PermissionSection section, User user, PermissionFlag flags)\n        {\n            Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key)\n            {\n                return new Permission(key);\n            });\n\n            permission.SetPermission(user, flags);\n        }\n\n        public void SetPermission(PermissionSection section, string subItemName, User user, PermissionFlag flags)\n        {\n            Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key)\n            {\n                return new Permission(key);\n            });\n\n            permission.SetSubItemPermission(subItemName, user, flags);\n        }\n\n        public void SetPermission(PermissionSection section, Group group, PermissionFlag flags)\n        {\n            Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key)\n            {\n                return new Permission(key);\n            });\n\n            permission.SetPermission(group, flags);\n        }\n\n        public void SetPermission(PermissionSection section, string subItemName, Group group, PermissionFlag flags)\n        {\n            Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key)\n            {\n                return new Permission(key);\n            });\n\n            permission.SetSubItemPermission(subItemName, group, flags);\n        }\n\n        public bool RemovePermission(PermissionSection section, User user)\n        {\n            return _permissions.TryGetValue(section, out Permission permission) && permission.RemovePermission(user);\n        }\n\n        public bool RemovePermission(PermissionSection section, string subItemName, User user)\n        {\n            return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveSubItemPermission(subItemName, user);\n        }\n\n        public bool RemovePermission(PermissionSection section, Group group)\n        {\n            return _permissions.TryGetValue(section, out Permission permission) && permission.RemovePermission(group);\n        }\n\n        public bool RemovePermission(PermissionSection section, string subItemName, Group group)\n        {\n            return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveSubItemPermission(subItemName, group);\n        }\n\n        public bool RemoveAllPermissions(PermissionSection section, string subItemName)\n        {\n            return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveAllSubItemPermissions(subItemName);\n        }\n\n        public bool IsPermitted(PermissionSection section, User user, PermissionFlag flag)\n        {\n            return _permissions.TryGetValue(section, out Permission permission) && permission.IsPermitted(user, flag);\n        }\n\n        public bool IsPermitted(PermissionSection section, string subItemName, User user, PermissionFlag flag)\n        {\n            return _permissions.TryGetValue(section, out Permission permission) && permission.IsSubItemPermitted(subItemName, user, flag);\n        }\n\n        #endregion\n\n        #region properties\n\n        public ICollection<Group> Groups\n        { get { return _groups.Values; } }\n\n        public ICollection<User> Users\n        { get { return _users.Values; } }\n\n        public ICollection<Permission> Permissions\n        { get { return _permissions.Values; } }\n\n        public ICollection<UserSession> Sessions\n        { get { return _sessions.Values; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Auth/Group.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Auth\n{\n    class Group : IComparable<Group>\n    {\n        #region variables\n\n        public const string ADMINISTRATORS = \"Administrators\";\n        public const string EVERYONE = \"Everyone\";\n        public const string DNS_ADMINISTRATORS = \"DNS Administrators\";\n        public const string DHCP_ADMINISTRATORS = \"DHCP Administrators\";\n\n        string _name;\n        string _description;\n\n        #endregion\n\n        #region constructor\n\n        public Group(string name, string description)\n        {\n            Name = name;\n            Description = description;\n        }\n\n        public Group(BinaryReader bR)\n        {\n            switch (bR.ReadByte())\n            {\n                case 1:\n                    _name = bR.ReadShortString();\n                    _description = bR.ReadShortString();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"Invalid data or version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)1);\n            bW.WriteShortString(_name);\n            bW.WriteShortString(_description);\n        }\n\n        public override bool Equals(object obj)\n        {\n            if (obj is not Group other)\n                return false;\n\n            return _name.Equals(other._name, StringComparison.OrdinalIgnoreCase);\n        }\n\n        public override int GetHashCode()\n        {\n            return HashCode.Combine(_name);\n        }\n\n        public override string ToString()\n        {\n            return _name;\n        }\n\n        public int CompareTo(Group other)\n        {\n            return _name.CompareTo(other._name);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Name\n        {\n            get { return _name; }\n            set\n            {\n                if (string.IsNullOrWhiteSpace(value))\n                    throw new ArgumentException(\"Group name cannot be null or empty.\", nameof(Name));\n\n                if (value.Length > 255)\n                    throw new ArgumentException(\"Group name length cannot exceed 255 characters.\", nameof(Name));\n\n                switch (_name?.ToLowerInvariant())\n                {\n                    case \"everyone\":\n                    case \"administrators\":\n                    case \"dns administrators\":\n                    case \"dhcp administrators\":\n                        throw new InvalidOperationException(\"Access was denied.\");\n\n                    default:\n                        _name = value;\n                        break;\n                }\n            }\n        }\n\n        public string Description\n        {\n            get { return _description; }\n            set\n            {\n                if (string.IsNullOrWhiteSpace(value))\n                    _description = \"\";\n                else if (value.Length > 255)\n                    throw new ArgumentException(\"Group description length cannot exceed 255 characters.\", nameof(Description));\n                else\n                    _description = value;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Auth/Permission.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Auth\n{\n    enum PermissionSection : byte\n    {\n        Unknown = 0,\n        Dashboard = 1,\n        Zones = 2,\n        Cache = 3,\n        Allowed = 4,\n        Blocked = 5,\n        Apps = 6,\n        DnsClient = 7,\n        Settings = 8,\n        DhcpServer = 9,\n        Administration = 10,\n        Logs = 11\n    }\n\n    [Flags]\n    enum PermissionFlag : byte\n    {\n        None = 0,\n        View = 1,\n        Modify = 2,\n        Delete = 4,\n        ViewModify = 3,\n        ViewModifyDelete = 7\n    }\n\n    class Permission : IComparable<Permission>\n    {\n        #region variables\n\n        readonly PermissionSection _section;\n        readonly string _subItemName;\n\n        readonly ConcurrentDictionary<User, PermissionFlag> _userPermissions;\n        readonly ConcurrentDictionary<Group, PermissionFlag> _groupPermissions;\n\n        readonly ConcurrentDictionary<string, Permission> _subItemPermissions;\n\n        #endregion\n\n        #region constructor\n\n        public Permission(PermissionSection section, string subItemName = null)\n        {\n            _section = section;\n            _subItemName = subItemName;\n\n            _userPermissions = new ConcurrentDictionary<User, PermissionFlag>(1, 1);\n            _groupPermissions = new ConcurrentDictionary<Group, PermissionFlag>(1, 1);\n\n            _subItemPermissions = new ConcurrentDictionary<string, Permission>(1, 1);\n        }\n\n        public Permission(BinaryReader bR, IReadOnlyDictionary<string, User> users, IReadOnlyDictionary<string, Group> groups)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                    _section = (PermissionSection)bR.ReadByte();\n\n                    {\n                        int count = bR.ReadByte();\n                        _userPermissions = new ConcurrentDictionary<User, PermissionFlag>(1, count);\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            string username = bR.ReadShortString().ToLowerInvariant();\n                            PermissionFlag flag = (PermissionFlag)bR.ReadByte();\n\n                            if (users.TryGetValue(username, out User user))\n                                _userPermissions.TryAdd(user, flag);\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadByte();\n                        _groupPermissions = new ConcurrentDictionary<Group, PermissionFlag>(1, count);\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            string groupName = bR.ReadShortString().ToLowerInvariant();\n                            PermissionFlag flag = (PermissionFlag)bR.ReadByte();\n\n                            if (groups.TryGetValue(groupName, out Group group))\n                                _groupPermissions.TryAdd(group, flag);\n                        }\n                    }\n\n                    {\n                        int count;\n\n                        if (version >= 2)\n                            count = bR.ReadInt32();\n                        else\n                            count = bR.ReadByte();\n\n                        _subItemPermissions = new ConcurrentDictionary<string, Permission>(1, count);\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            string subItemName = bR.ReadShortString();\n                            Permission subItemPermission = new Permission(bR, users, groups);\n\n                            _subItemPermissions.TryAdd(subItemName.ToLowerInvariant(), subItemPermission);\n                        }\n                    }\n\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"Invalid data or version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void SetPermission(User user, PermissionFlag flags)\n        {\n            _userPermissions[user] = flags;\n        }\n\n        public void SyncPermissions(IReadOnlyDictionary<User, PermissionFlag> userPermissions)\n        {\n            //remove non-existent permissions\n            foreach (KeyValuePair<User, PermissionFlag> userPermission in _userPermissions)\n            {\n                if (!userPermissions.ContainsKey(userPermission.Key))\n                    _userPermissions.TryRemove(userPermission.Key, out _);\n            }\n\n            //set new permissions\n            foreach (KeyValuePair<User, PermissionFlag> userPermission in userPermissions)\n                _userPermissions[userPermission.Key] = userPermission.Value;\n        }\n\n        public void SetSubItemPermission(string subItemName, User user, PermissionFlag flags)\n        {\n            Permission subItemPermission = _subItemPermissions.GetOrAdd(subItemName.ToLowerInvariant(), delegate (string key)\n            {\n                return new Permission(_section, key);\n            });\n\n            subItemPermission.SetPermission(user, flags);\n        }\n\n        public void SetPermission(Group group, PermissionFlag flags)\n        {\n            _groupPermissions[group] = flags;\n        }\n\n        public void SyncPermissions(IReadOnlyDictionary<Group, PermissionFlag> groupPermissions)\n        {\n            //remove non-existent permissions\n            foreach (KeyValuePair<Group, PermissionFlag> groupPermission in _groupPermissions)\n            {\n                if (!groupPermissions.ContainsKey(groupPermission.Key))\n                    _groupPermissions.TryRemove(groupPermission.Key, out _);\n            }\n\n            //set new permissions\n            foreach (KeyValuePair<Group, PermissionFlag> groupPermission in groupPermissions)\n                _groupPermissions[groupPermission.Key] = groupPermission.Value;\n        }\n\n        public void SetSubItemPermission(string subItemName, Group group, PermissionFlag flags)\n        {\n            Permission subItemPermission = _subItemPermissions.GetOrAdd(subItemName.ToLowerInvariant(), delegate (string key)\n            {\n                return new Permission(_section, key);\n            });\n\n            subItemPermission.SetPermission(group, flags);\n        }\n\n        public bool RemovePermission(User user)\n        {\n            return _userPermissions.TryRemove(user, out _);\n        }\n\n        public bool RemoveSubItemPermission(string subItemName, User user)\n        {\n            return _subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission) && subItemPermission.RemovePermission(user);\n        }\n\n        public bool RemovePermission(Group group)\n        {\n            return _groupPermissions.TryRemove(group, out _);\n        }\n\n        public bool RemoveSubItemPermission(string subItemName, Group group)\n        {\n            return _subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission) && subItemPermission.RemovePermission(group);\n        }\n\n        public bool RemoveAllSubItemPermissions(User user)\n        {\n            bool removed = false;\n\n            foreach (KeyValuePair<string, Permission> subItemPermission in _subItemPermissions)\n            {\n                if (subItemPermission.Value.RemovePermission(user))\n                    removed = true;\n            }\n\n            return removed;\n        }\n\n        public bool RemoveAllSubItemPermissions(Group group)\n        {\n            bool removed = false;\n\n            foreach (KeyValuePair<string, Permission> subItemPermission in _subItemPermissions)\n            {\n                if (subItemPermission.Value.RemovePermission(group))\n                    removed = true;\n            }\n\n            return removed;\n        }\n\n        public bool RemoveAllSubItemPermissions(string subItemName)\n        {\n            return _subItemPermissions.TryRemove(subItemName, out _);\n        }\n\n        public Permission GetSubItemPermission(string subItemName)\n        {\n            if (_subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission))\n                return subItemPermission;\n\n            return null;\n        }\n\n        public bool IsPermitted(User user, PermissionFlag flag)\n        {\n            if (_userPermissions.TryGetValue(user, out PermissionFlag userPermissions) && userPermissions.HasFlag(flag))\n                return true;\n\n            foreach (Group group in user.MemberOfGroups)\n            {\n                if (_groupPermissions.TryGetValue(group, out PermissionFlag groupPermissions) && groupPermissions.HasFlag(flag))\n                    return true;\n            }\n\n            return false;\n        }\n\n        public bool IsSubItemPermitted(string subItemName, User user, PermissionFlag flag)\n        {\n            return _subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission) && subItemPermission.IsPermitted(user, flag);\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)2);\n            bW.Write((byte)_section);\n\n            {\n                bW.Write(Convert.ToByte(_userPermissions.Count));\n\n                foreach (KeyValuePair<User, PermissionFlag> userPermission in _userPermissions)\n                {\n                    bW.WriteShortString(userPermission.Key.Username);\n                    bW.Write((byte)userPermission.Value);\n                }\n            }\n\n            {\n                bW.Write(Convert.ToByte(_groupPermissions.Count));\n\n                foreach (KeyValuePair<Group, PermissionFlag> groupPermission in _groupPermissions)\n                {\n                    bW.WriteShortString(groupPermission.Key.Name);\n                    bW.Write((byte)groupPermission.Value);\n                }\n            }\n\n            {\n                bW.Write(_subItemPermissions.Count);\n\n                foreach (KeyValuePair<string, Permission> subItemPermission in _subItemPermissions)\n                {\n                    bW.WriteShortString(subItemPermission.Key);\n                    subItemPermission.Value.WriteTo(bW);\n                }\n            }\n        }\n\n        public int CompareTo(Permission other)\n        {\n            return _section.CompareTo(other._section);\n        }\n\n        #endregion\n\n        #region properties\n\n        public PermissionSection Section\n        { get { return _section; } }\n\n        public string SubItemName\n        { get { return _subItemName; } }\n\n        public IReadOnlyDictionary<User, PermissionFlag> UserPermissions\n        { get { return _userPermissions; } }\n\n        public IReadOnlyDictionary<Group, PermissionFlag> GroupPermissions\n        { get { return _groupPermissions; } }\n\n        public IReadOnlyDictionary<string, Permission> SubItemPermissions\n        { get { return _subItemPermissions; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Auth/User.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Security.Cryptography;\nusing System.Text;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Security.OTP;\n\nnamespace DnsServerCore.Auth\n{\n    enum UserPasswordHashType : byte\n    {\n        Unknown = 0,\n        OldScheme = 1,\n        PBKDF2_SHA256 = 2\n    }\n\n    class User : IComparable<User>\n    {\n        #region variables\n\n        public const int DEFAULT_ITERATIONS = 100000;\n\n        string _displayName;\n        string _username;\n        UserPasswordHashType _passwordHashType;\n        int _iterations;\n        byte[] _salt;\n        string _passwordHash;\n        AuthenticatorKeyUri _totpKeyUri;\n        bool _totpEnabled;\n        bool _disabled;\n        int _sessionTimeoutSeconds = 30 * 60; //default 30 mins\n\n        DateTime _previousSessionLoggedOn;\n        IPAddress _previousSessionRemoteAddress;\n        DateTime _recentSessionLoggedOn;\n        IPAddress _recentSessionRemoteAddress;\n\n        readonly ConcurrentDictionary<string, Group> _memberOfGroups;\n\n        #endregion\n\n        #region constructor\n\n        public User(string displayName, string username, string password, int iterations = DEFAULT_ITERATIONS)\n        {\n            Username = username;\n            DisplayName = displayName;\n\n            ChangePassword(password, iterations);\n\n            _previousSessionRemoteAddress = IPAddress.Any;\n            _recentSessionRemoteAddress = IPAddress.Any;\n\n            _memberOfGroups = new ConcurrentDictionary<string, Group>(1, 2);\n        }\n\n        public User(BinaryReader bR, IReadOnlyDictionary<string, Group> groups)\n        {\n            int version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                    _displayName = bR.ReadShortString();\n                    _username = bR.ReadShortString();\n                    _passwordHashType = (UserPasswordHashType)bR.ReadByte();\n                    _iterations = bR.ReadInt32();\n                    _salt = bR.ReadBuffer();\n                    _passwordHash = bR.ReadShortString();\n\n                    if (version >= 2)\n                    {\n                        string otpKeyUri = bR.ReadString();\n                        if (!string.IsNullOrEmpty(otpKeyUri))\n                            _totpKeyUri = AuthenticatorKeyUri.Parse(otpKeyUri);\n\n                        _totpEnabled = bR.ReadBoolean();\n                    }\n\n                    _disabled = bR.ReadBoolean();\n                    _sessionTimeoutSeconds = bR.ReadInt32();\n\n                    _previousSessionLoggedOn = bR.ReadDateTime();\n                    _previousSessionRemoteAddress = IPAddressExtensions.ReadFrom(bR);\n                    _recentSessionLoggedOn = bR.ReadDateTime();\n                    _recentSessionRemoteAddress = IPAddressExtensions.ReadFrom(bR);\n\n                    {\n                        int count = bR.ReadByte();\n                        _memberOfGroups = new ConcurrentDictionary<string, Group>(1, count);\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            if (groups.TryGetValue(bR.ReadShortString().ToLowerInvariant(), out Group group))\n                                _memberOfGroups.TryAdd(group.Name.ToLowerInvariant(), group);\n                        }\n                    }\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"Invalid data or version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        internal void RenameGroup(string oldName)\n        {\n            if (_memberOfGroups.TryRemove(oldName.ToLowerInvariant(), out Group renamedGroup))\n                _memberOfGroups.TryAdd(renamedGroup.Name.ToLowerInvariant(), renamedGroup);\n        }\n\n        #endregion\n\n        #region public\n\n        public string GetPasswordHashFor(string password)\n        {\n            switch (_passwordHashType)\n            {\n                case UserPasswordHashType.OldScheme:\n                    using (HMAC hmac = new HMACSHA256(Encoding.UTF8.GetBytes(password)))\n                    {\n                        return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(_username))).ToLowerInvariant();\n                    }\n\n                case UserPasswordHashType.PBKDF2_SHA256:\n                    return Convert.ToHexString(Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), _salt, _iterations, HashAlgorithmName.SHA256, 32)).ToLowerInvariant();\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public void ChangePassword(string newPassword, int iterations = DEFAULT_ITERATIONS)\n        {\n            _passwordHashType = UserPasswordHashType.PBKDF2_SHA256;\n            _iterations = iterations;\n\n            _salt = new byte[32];\n            RandomNumberGenerator.Fill(_salt);\n\n            _passwordHash = GetPasswordHashFor(newPassword);\n        }\n\n        public void LoadOldSchemeCredentials(string passwordHash)\n        {\n            _passwordHashType = UserPasswordHashType.OldScheme;\n            _passwordHash = passwordHash;\n        }\n\n        public AuthenticatorKeyUri InitializedTOTP(string issuer)\n        {\n            if (_totpEnabled)\n                throw new InvalidOperationException(\"Time-based one-time password (TOTP) is already enabled for user: \" + _username);\n\n            _totpKeyUri = AuthenticatorKeyUri.Generate(issuer, _username);\n\n            return _totpKeyUri;\n        }\n\n        public void EnableTOTP(string totp)\n        {\n            if (_totpKeyUri is null)\n                throw new InvalidOperationException(\"Time-based one-time password (TOTP) was not initialized for user: \" + _username);\n\n            if (_totpEnabled)\n                throw new InvalidOperationException(\"Time-based one-time password (TOTP) is already enabled for user: \" + _username);\n\n            Authenticator authenticator = new Authenticator(_totpKeyUri);\n\n            if (!authenticator.IsTOTPValid(totp))\n                throw new Exception(\"Invalid time-based one-time password (TOTP) was attempted for user: \" + _username);\n\n            _totpEnabled = true;\n        }\n\n        public void DisableTOTP()\n        {\n            if (!_totpEnabled)\n                throw new InvalidOperationException(\"Time-based one-time password (TOTP) is already disabled for user: \" + _username);\n\n            _totpKeyUri = null;\n            _totpEnabled = false;\n        }\n\n        public void LoggedInFrom(IPAddress remoteAddress)\n        {\n            if (remoteAddress.IsIPv4MappedToIPv6)\n                remoteAddress = remoteAddress.MapToIPv4();\n\n            _previousSessionLoggedOn = _recentSessionLoggedOn;\n            _previousSessionRemoteAddress = _recentSessionRemoteAddress;\n\n            _recentSessionLoggedOn = DateTime.UtcNow;\n            _recentSessionRemoteAddress = remoteAddress;\n        }\n\n        public void AddToGroup(Group group)\n        {\n            if (_memberOfGroups.Count == 255)\n                throw new InvalidOperationException(\"Cannot add user to group: user can be member of max 255 groups.\");\n\n            _memberOfGroups.TryAdd(group.Name.ToLowerInvariant(), group);\n        }\n\n        public bool RemoveFromGroup(Group group)\n        {\n            if (group.Name.Equals(\"everyone\", StringComparison.OrdinalIgnoreCase))\n                throw new InvalidOperationException(\"Access was denied.\");\n\n            return _memberOfGroups.TryRemove(group.Name.ToLowerInvariant(), out _);\n        }\n\n        public void SyncGroups(IReadOnlyDictionary<string, Group> groups)\n        {\n            //remove non-existent groups\n            foreach (KeyValuePair<string, Group> group in _memberOfGroups)\n            {\n                if (!groups.ContainsKey(group.Key))\n                    _memberOfGroups.TryRemove(group.Key, out _);\n            }\n\n            //set new groups\n            foreach (KeyValuePair<string, Group> group in groups)\n                _memberOfGroups[group.Key] = group.Value;\n        }\n\n        public bool IsMemberOfGroup(Group group)\n        {\n            return _memberOfGroups.ContainsKey(group.Name.ToLowerInvariant());\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)2);\n            bW.WriteShortString(_displayName);\n            bW.WriteShortString(_username);\n            bW.Write((byte)_passwordHashType);\n            bW.Write(_iterations);\n            bW.WriteBuffer(_salt);\n            bW.WriteShortString(_passwordHash);\n\n            if (_totpKeyUri is null)\n                bW.Write(\"\");\n            else\n                bW.Write(_totpKeyUri.ToString());\n\n            bW.Write(_totpEnabled);\n            bW.Write(_disabled);\n            bW.Write(_sessionTimeoutSeconds);\n\n            bW.Write(_previousSessionLoggedOn);\n            IPAddressExtensions.WriteTo(_previousSessionRemoteAddress, bW);\n            bW.Write(_recentSessionLoggedOn);\n            IPAddressExtensions.WriteTo(_recentSessionRemoteAddress, bW);\n\n            bW.Write(Convert.ToByte(_memberOfGroups.Count));\n\n            foreach (KeyValuePair<string, Group> group in _memberOfGroups)\n                bW.WriteShortString(group.Value.Name.ToLowerInvariant());\n        }\n\n        public override bool Equals(object obj)\n        {\n            if (obj is not User other)\n                return false;\n\n            return _username.Equals(other._username, StringComparison.OrdinalIgnoreCase);\n        }\n\n        public override int GetHashCode()\n        {\n            return HashCode.Combine(_username);\n        }\n\n        public override string ToString()\n        {\n            return _username;\n        }\n\n        public int CompareTo(User other)\n        {\n            return _username.CompareTo(other._username);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string DisplayName\n        {\n            get { return _displayName; }\n            set\n            {\n                if (string.IsNullOrWhiteSpace(value))\n                    _displayName = _username;\n                else if (value.Length > 255)\n                    throw new ArgumentException(\"Display name length cannot exceed 255 characters.\", nameof(DisplayName));\n                else\n                    _displayName = value;\n            }\n        }\n\n        public string Username\n        {\n            get { return _username; }\n            set\n            {\n                if (_passwordHashType == UserPasswordHashType.OldScheme)\n                    throw new InvalidOperationException(\"Cannot change username when using old password hash scheme. Change password once and try again.\");\n\n                if (string.IsNullOrWhiteSpace(value))\n                    throw new ArgumentException(\"Username cannot be null or empty.\", nameof(Username));\n\n                if (value.Length > 255)\n                    throw new ArgumentException(\"Username length cannot exceed 255 characters.\", nameof(Username));\n\n                foreach (char c in value)\n                {\n                    if ((c >= 97) && (c <= 122)) //[a-z]\n                        continue;\n\n                    if ((c >= 65) && (c <= 90)) //[A-Z]\n                        continue;\n\n                    if ((c >= 48) && (c <= 57)) //[0-9]\n                        continue;\n\n                    if (c == '-')\n                        continue;\n\n                    if (c == '_')\n                        continue;\n\n                    if (c == '.')\n                        continue;\n\n                    throw new ArgumentException(\"Username can contain only alpha numeric, '-', '_', or '.' characters.\", nameof(Username));\n                }\n\n                _username = value.ToLowerInvariant();\n            }\n        }\n\n        public UserPasswordHashType PasswordHashType\n        { get { return _passwordHashType; } }\n\n        public string PasswordHash\n        { get { return _passwordHash; } }\n\n        public AuthenticatorKeyUri TOTPKeyUri\n        { get { return _totpKeyUri; } }\n\n        public bool TOTPEnabled\n        { get { return _totpEnabled; } }\n\n        public bool Disabled\n        {\n            get { return _disabled; }\n            set { _disabled = value; }\n        }\n\n        public int SessionTimeoutSeconds\n        {\n            get { return _sessionTimeoutSeconds; }\n            set\n            {\n                if ((value < 0) || (value > 604800))\n                    throw new ArgumentOutOfRangeException(nameof(SessionTimeoutSeconds), \"Session timeout value must be between 0-604800 seconds.\");\n\n                if ((value > 0) && (value < 60))\n                    value = 60; //to prevent issues with too low timeout set by mistake\n\n                _sessionTimeoutSeconds = value;\n            }\n        }\n\n        public DateTime PreviousSessionLoggedOn\n        { get { return _previousSessionLoggedOn; } }\n\n        public IPAddress PreviousSessionRemoteAddress\n        { get { return _previousSessionRemoteAddress; } }\n\n        public DateTime RecentSessionLoggedOn\n        { get { return _recentSessionLoggedOn; } }\n\n        public IPAddress RecentSessionRemoteAddress\n        { get { return _recentSessionRemoteAddress; } }\n\n        public ICollection<Group> MemberOfGroups\n        { get { return _memberOfGroups.Values; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Auth/UserSession.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Security.Cryptography;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerCore.Auth\n{\n    enum UserSessionType : byte\n    {\n        Unknown = 0,\n        Standard = 1,\n        ApiToken = 2\n    }\n\n    class UserSession : IComparable<UserSession>\n    {\n        #region variables\n\n        readonly string _token;\n        readonly UserSessionType _type;\n        readonly string _tokenName;\n        User _user;\n        DateTime _lastSeen;\n        IPAddress _lastSeenRemoteAddress;\n        string _lastSeenUserAgent;\n\n        #endregion\n\n        #region constructor\n\n        public UserSession(UserSessionType type, string tokenName, User user, IPAddress remoteAddress, string lastSeenUserAgent)\n        {\n            if ((tokenName is not null) && (tokenName.Length > 255))\n                throw new ArgumentOutOfRangeException(nameof(tokenName), \"Token name length cannot exceed 255 characters.\");\n\n            if (remoteAddress.IsIPv4MappedToIPv6)\n                remoteAddress = remoteAddress.MapToIPv4();\n\n            Span<byte> tokenBytes = stackalloc byte[32];\n            RandomNumberGenerator.Fill(tokenBytes);\n            _token = Convert.ToHexString(tokenBytes).ToLowerInvariant();\n\n            _type = type;\n            _tokenName = tokenName;\n            _user = user;\n            _lastSeen = DateTime.UtcNow;\n            _lastSeenRemoteAddress = remoteAddress;\n            _lastSeenUserAgent = lastSeenUserAgent;\n\n            if ((_lastSeenUserAgent is not null) && (_lastSeenUserAgent.Length > 255))\n                _lastSeenUserAgent = _lastSeenUserAgent.Substring(0, 255);\n        }\n\n        public UserSession(BinaryReader bR, IReadOnlyDictionary<string, User> users)\n        {\n            switch (bR.ReadByte())\n            {\n                case 1:\n                    _token = bR.ReadShortString();\n                    _type = (UserSessionType)bR.ReadByte();\n\n                    _tokenName = bR.ReadShortString();\n                    if (_tokenName.Length == 0)\n                        _tokenName = null;\n\n                    users.TryGetValue(bR.ReadShortString().ToLowerInvariant(), out _user);\n\n                    _lastSeen = bR.ReadDateTime();\n                    _lastSeenRemoteAddress = IPAddressExtensions.ReadFrom(bR);\n\n                    _lastSeenUserAgent = bR.ReadShortString();\n                    if (_lastSeenUserAgent.Length == 0)\n                        _lastSeenUserAgent = null;\n\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"Invalid data or version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void UpdateUserObject(IReadOnlyDictionary<string, User> users)\n        {\n            if (users.TryGetValue(_user.Username, out User user))\n                _user = user;\n        }\n\n        public void UpdateLastSeen(IPAddress remoteAddress, string lastSeenUserAgent)\n        {\n            if (remoteAddress.IsIPv4MappedToIPv6)\n                remoteAddress = remoteAddress.MapToIPv4();\n\n            _lastSeen = DateTime.UtcNow;\n            _lastSeenRemoteAddress = remoteAddress;\n            _lastSeenUserAgent = lastSeenUserAgent;\n\n            if ((_lastSeenUserAgent is not null) && (_lastSeenUserAgent.Length > 255))\n                _lastSeenUserAgent = _lastSeenUserAgent.Substring(0, 255);\n        }\n\n        public bool HasExpired()\n        {\n            if (_type == UserSessionType.ApiToken)\n                return false;\n\n            if (_user.SessionTimeoutSeconds == 0)\n                return false;\n\n            return _lastSeen.AddSeconds(_user.SessionTimeoutSeconds) < DateTime.UtcNow;\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)1);\n            bW.WriteShortString(_token);\n            bW.Write((byte)_type);\n\n            if (_tokenName is null)\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_tokenName);\n\n            bW.WriteShortString(_user.Username);\n            bW.Write(_lastSeen);\n            _lastSeenRemoteAddress.WriteTo(bW);\n\n            if (_lastSeenUserAgent is null)\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_lastSeenUserAgent);\n        }\n\n        public int CompareTo(UserSession other)\n        {\n            return other._lastSeen.CompareTo(_lastSeen);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Token\n        { get { return _token; } }\n\n        public UserSessionType Type\n        { get { return _type; } }\n\n        public string TokenName\n        { get { return _tokenName; } }\n\n        public User User\n        { get { return _user; } }\n\n        public DateTime LastSeen\n        { get { return _lastSeen; } }\n\n        public IPAddress LastSeenRemoteAddress\n        { get { return _lastSeenRemoteAddress; } }\n\n        public string LastSeenUserAgent\n        { get { return _lastSeenUserAgent; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Cluster/ClusterManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Dns;\nusing DnsServerCore.Dns.Dnssec;\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Zones;\nusing DnsServerCore.HttpApi;\nusing DnsServerCore.HttpApi.Models;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Cluster\n{\n    sealed class ClusterManager : IDisposable\n    {\n        #region variables\n\n        const ushort HEARTBEAT_REFRESH_INTERVAL_SECONDS = 30;\n        const ushort HEARTBEAT_RETRY_INTERVAL_SECONDS = 10;\n        const ushort CONFIG_REFRESH_INTERVAL_SECONDS = 900;\n        const ushort CONFIG_RETRY_INTERVAL_SECONDS = 60;\n\n        readonly DnsWebService _dnsWebService;\n\n        string _clusterDomain;\n        ushort _heartbeatRefreshIntervalSeconds;\n        ushort _heartbeatRetryIntervalSeconds;\n        ushort _configRefreshIntervalSeconds;\n        ushort _configRetryIntervalSeconds;\n        DateTime _configLastSynced;\n\n        IReadOnlyDictionary<int, ClusterNode> _clusterNodes;\n\n        readonly SemaphoreSlim _configRefreshLock = new SemaphoreSlim(1, 1);\n        readonly Timer _configRefreshTimer;\n        bool _configRefreshTimerTriggered;\n        IReadOnlyCollection<string> _configRefreshIncludeZones;\n        const int CONFIG_REFRESH_TIMER_INTERVAL = 5000;\n\n        readonly Timer _notifyAllSecondaryNodesTimer;\n        bool _notifyAllSecondaryNodesTimerTriggered;\n        const int NOTIFY_ALL_SECONDARY_NODES_TIMER_INTERVAL = 5000;\n\n        readonly Timer _clusterUpdateForSecondaryNodeChangesTimer;\n        bool _clusterUpdateForSecondaryNodeChangesTimerTriggered;\n        const int CLUSTER_UPDATE_FOR_SECONDARY_NODE_CHANGES_TIMER_INTERVAL = 5000;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        volatile int _recordUpdateForMemberZonesId;\n\n        #endregion\n\n        #region constructor\n\n        public ClusterManager(DnsWebService dnsWebService)\n        {\n            _dnsWebService = dnsWebService;\n\n            _configRefreshTimer = new Timer(ConfigRefreshTimerCallbackAsync);\n            _notifyAllSecondaryNodesTimer = new Timer(NotifyAllSecondaryNodesTimerCallbackAsync);\n            _clusterUpdateForSecondaryNodeChangesTimer = new Timer(ClusterUpdateForSecondaryNodeChangesTimerCallbackAsync);\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveConfigFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService.LogManager.Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _configRefreshTimer?.Dispose();\n            _notifyAllSecondaryNodesTimer?.Dispose();\n            _clusterUpdateForSecondaryNodeChangesTimer?.Dispose();\n\n            DisposeAllNodes();\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                if (_pendingSave)\n                {\n                    try\n                    {\n                        SaveConfigFileInternal();\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsWebService.LogManager.Write(ex);\n                    }\n                    finally\n                    {\n                        _pendingSave = false;\n                    }\n                }\n            }\n\n            _configRefreshLock?.Dispose();\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region config\n\n        public void LoadConfigFile()\n        {\n            string configFile = Path.Combine(_dnsWebService.ConfigFolder, \"cluster.config\");\n\n            try\n            {\n                DisposeAllNodes(); //dispose existing nodes, if any\n\n                using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS);\n                }\n\n                InitializeHeartbeatTimerFor(_clusterNodes);\n                UpdateConfigRefreshTimer();\n\n                _dnsWebService.LogManager.Write(\"DNS Server Cluster config file was loaded: \" + configFile);\n            }\n            catch (FileNotFoundException)\n            {\n                //do nothing\n            }\n            catch (Exception ex)\n            {\n                _dnsWebService.LogManager.Write(\"DNS Server encountered an error while loading the Cluster config file: \" + configFile + \"\\r\\n\" + ex.ToString());\n            }\n        }\n\n        public void LoadConfig(Stream s)\n        {\n            lock (_saveLock)\n            {\n                DisposeAllNodes(); //dispose existing nodes, if any\n\n                ReadConfigFrom(s);\n\n                InitializeHeartbeatTimerFor(_clusterNodes);\n                UpdateConfigRefreshTimer();\n\n                //save config file\n                SaveConfigFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void UpdateConfigRefreshTimer(int refreshInterval = CONFIG_REFRESH_TIMER_INTERVAL)\n        {\n            //ensure that the new refresh interval is applied using lock\n            _configRefreshLock.Wait();\n            try\n            {\n                if (ClusterInitialized && (GetSelfNode().Type == ClusterNodeType.Secondary))\n                    _configRefreshTimer.Change(refreshInterval, Timeout.Infinite); //start config refresh timer only for secondary nodes\n                else\n                    _configRefreshTimer.Change(Timeout.Infinite, Timeout.Infinite);\n            }\n            finally\n            {\n                _configRefreshLock.Release();\n            }\n        }\n\n        private void StopConfigRefreshTimer()\n        {\n            //ensure that the timer is stopped using lock\n            _configRefreshLock.Wait();\n            try\n            {\n                _configRefreshTimer.Change(Timeout.Infinite, Timeout.Infinite);\n            }\n            finally\n            {\n                _configRefreshLock.Release();\n            }\n        }\n\n        private void SaveConfigFileInternal()\n        {\n            if (!ClusterInitialized)\n                throw new InvalidOperationException();\n\n            string configFile = Path.Combine(_dnsWebService.ConfigFolder, \"cluster.config\");\n\n            using (MemoryStream mS = new MemoryStream())\n            {\n                //serialize config\n                WriteConfigTo(mS);\n\n                //write config\n                mS.Position = 0;\n\n                using (FileStream fS = new FileStream(configFile, FileMode.Create, FileAccess.Write))\n                {\n                    mS.CopyTo(fS);\n                }\n            }\n\n            _dnsWebService.LogManager.Write(\"DNS Server Cluster config file was saved: \" + configFile);\n        }\n\n        public void SaveConfigFile()\n        {\n            if (!ClusterInitialized)\n                throw new InvalidOperationException();\n\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void UnloadAndDeleteConfigFile()\n        {\n            StopConfigRefreshTimer();\n\n            DisposeAllNodes(); //dispose existing nodes, if any\n\n            lock (_saveLock)\n            {\n                //unload\n                _clusterDomain = null;\n                _configLastSynced = default;\n                _clusterNodes = null;\n\n                //delete config file\n                string configFile = Path.Combine(_dnsWebService.ConfigFolder, \"cluster.config\");\n\n                try\n                {\n                    if (File.Exists(configFile))\n                    {\n                        File.Delete(configFile);\n\n                        _dnsWebService.LogManager.Write(\"DNS Server Cluster config file was deleted: \" + configFile);\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsWebService.LogManager.Write(\"DNS Server encountered an error while deleting the Cluster config file: \" + configFile + \"\\r\\n\" + ex.ToString());\n                }\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void ReadConfigFrom(Stream s)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"CL\") //format\n                throw new InvalidDataException(\"DNS Server Cluster config file format is invalid.\");\n\n            int version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    _clusterDomain = bR.ReadString();\n                    _heartbeatRefreshIntervalSeconds = bR.ReadUInt16();\n                    _heartbeatRetryIntervalSeconds = bR.ReadUInt16();\n                    _configRefreshIntervalSeconds = bR.ReadUInt16();\n                    _configRetryIntervalSeconds = bR.ReadUInt16();\n                    _configLastSynced = bR.ReadDateTime();\n\n                    Dictionary<int, ClusterNode> clusterNodes = null;\n                    int count = bR.ReadByte();\n\n                    if (count > 0)\n                    {\n                        clusterNodes = new Dictionary<int, ClusterNode>(count);\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            ClusterNode node = new ClusterNode(this, bR);\n                            clusterNodes.TryAdd(node.Id, node);\n                        }\n                    }\n\n                    _clusterNodes = clusterNodes;\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"DNS Server Cluster config version not supported.\");\n            }\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"CL\")); //format\n            bW.Write((byte)1); //version\n\n            bW.Write(_clusterDomain);\n            bW.Write(_heartbeatRefreshIntervalSeconds);\n            bW.Write(_heartbeatRetryIntervalSeconds);\n            bW.Write(_configRefreshIntervalSeconds);\n            bW.Write(_configRetryIntervalSeconds);\n            bW.Write(_configLastSynced);\n\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n            if (clusterNodes is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(clusterNodes.Count));\n\n                foreach (KeyValuePair<int, ClusterNode> node in clusterNodes)\n                    node.Value.WriteTo(bW);\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private void DisposeAllNodes()\n        {\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n            if (clusterNodes is not null)\n            {\n                foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n                    clusterNode.Value.Dispose();\n            }\n        }\n\n        private static void InitializeHeartbeatTimerFor(IReadOnlyDictionary<int, ClusterNode> clusterNodes)\n        {\n            //start heartbeat timers for all nodes except self node\n            foreach (KeyValuePair<int, ClusterNode> node in clusterNodes)\n            {\n                if (node.Value.State == ClusterNodeState.Self)\n                    continue;\n\n                node.Value.InitializeHeartbeatTimer();\n            }\n        }\n\n        private void UpdateHeartbeatTimerForAllClusterNodes()\n        {\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n            if (clusterNodes is not null)\n            {\n                foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n                {\n                    if (clusterNode.Value.State == ClusterNodeState.Self)\n                        continue;\n\n                    clusterNode.Value.UpdateHeartbeatTimer();\n                }\n            }\n        }\n\n        private void DeleteAllClusterConfig()\n        {\n            //delete cluster catalog zone\n            string clusterCatalogDomain = \"cluster-catalog.\" + _clusterDomain;\n\n            AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);\n            if (clusterCatalogZoneInfo is not null)\n            {\n                if (_dnsWebService.DnsServer.AuthZoneManager.DeleteZone(clusterCatalogZoneInfo, true))\n                    _dnsWebService.AuthManager.RemoveAllPermissions(PermissionSection.Zones, clusterCatalogDomain);\n            }\n\n            //remove TSIG key for cluster catalog zone\n            {\n                IReadOnlyDictionary<string, TsigKey> existingKeys = _dnsWebService.DnsServer.TsigKeys;\n                if (existingKeys is not null)\n                {\n                    Dictionary<string, TsigKey> updatedKeys = new Dictionary<string, TsigKey>(existingKeys);\n                    updatedKeys.Remove(clusterCatalogDomain);\n\n                    _dnsWebService.DnsServer.TsigKeys = updatedKeys;\n                }\n            }\n\n            //delete cluster API token\n            {\n                foreach (UserSession session in _dnsWebService.AuthManager.Sessions)\n                {\n                    if ((session.Type == UserSessionType.ApiToken) && (session.TokenName == _clusterDomain))\n                        _dnsWebService.AuthManager.DeleteSession(session.Token);\n                }\n            }\n\n            //finalize\n            if (_dnsWebService.DnsServer.ServerDomain.EndsWith(\".\" + _clusterDomain, StringComparison.OrdinalIgnoreCase))\n                _dnsWebService.DnsServer.ServerDomain = _dnsWebService.DnsServer.ServerDomain.Substring(0, _dnsWebService.DnsServer.ServerDomain.Length - (_clusterDomain.Length + 1));\n\n            //save all changes\n            _dnsWebService.DnsServer.SaveConfigFile();\n            _dnsWebService.AuthManager.SaveConfigFile();\n\n            UnloadAndDeleteConfigFile();\n        }\n\n        #endregion\n\n        #region primary node\n\n        public void InitializeCluster(string clusterDomain, IReadOnlyList<IPAddress> primaryNodeIpAddresses, UserSession session)\n        {\n            if (ClusterInitialized)\n                throw new DnsServerException(\"Failed to initialize Cluster: the Cluster is already initialized.\");\n\n            if (!_dnsWebService.IsWebServiceTlsEnabled)\n                throw new InvalidOperationException();\n\n            clusterDomain = clusterDomain.ToLowerInvariant();\n\n            //create self node\n            string serverDomain = _dnsWebService.DnsServer.ServerDomain;\n            if (!serverDomain.EndsWith(\".\" + clusterDomain, StringComparison.OrdinalIgnoreCase))\n            {\n                int x = serverDomain.IndexOf('.');\n                if (x < 0)\n                    serverDomain = serverDomain + \".\" + clusterDomain;\n                else\n                    serverDomain = string.Concat(serverDomain.AsSpan(0, x), \".\", clusterDomain);\n            }\n\n            Uri primaryNodeUrl = new Uri($\"https://{serverDomain}:{_dnsWebService.WebServiceTlsPort}/\");\n\n            ClusterNode selfPrimaryNode = new ClusterNode(this, RandomNumberGenerator.GetInt32(int.MaxValue), primaryNodeUrl, primaryNodeIpAddresses, ClusterNodeType.Primary, ClusterNodeState.Self);\n\n            //create cluster primary zone\n            AuthZoneInfo clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterDomain);\n            if (clusterZoneInfo is null)\n            {\n                clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreatePrimaryZone(clusterDomain);\n                if (clusterZoneInfo is null)\n                    throw new DnsServerException($\"Failed to initialize Cluster: failed to create the Cluster zone '{clusterDomain}'. Please try again.\");\n            }\n            else if (clusterZoneInfo.Type != AuthZoneType.Primary)\n            {\n                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.\");\n            }\n\n            //create cluster catalog zone\n            string clusterCatalogDomain = \"cluster-catalog.\" + clusterDomain;\n\n            AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);\n            if (clusterCatalogZoneInfo is null)\n            {\n                clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateCatalogZone(clusterCatalogDomain);\n                if (clusterCatalogZoneInfo is null)\n                    throw new DnsServerException($\"Failed to initialize Cluster: failed to create the Cluster Catalog zone '{clusterCatalogDomain}'. Please try again.\");\n            }\n            else if (clusterCatalogZoneInfo.Type != AuthZoneType.Catalog)\n            {\n                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.\");\n            }\n\n            //set cluster primary zone permissions\n            _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n            _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View);\n\n            //set cluster catalog zone permissions\n            _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n            _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View);\n\n            //ensure cluster zone is a member of cluster catalog zone\n            if (clusterZoneInfo.CatalogZoneName is null)\n                _dnsWebService.DnsServer.AuthZoneManager.AddCatalogMemberZone(clusterCatalogZoneInfo.Name, clusterZoneInfo);\n            else if (!clusterZoneInfo.CatalogZoneName.Equals(clusterCatalogZoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                _dnsWebService.DnsServer.AuthZoneManager.ChangeCatalogMemberZoneOwnership(clusterZoneInfo, clusterCatalogZoneInfo.Name);\n\n            //sign cluster zone\n            if (clusterZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned)\n            {\n                DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.KeySigningKey);\n                DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.ZoneSigningKey);\n                zskPrivateKey.RolloverDays = 90;\n\n                _dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZone(clusterZoneInfo.Name, kskPrivateKey, zskPrivateKey, 3600, false);\n            }\n\n            //create TSIG key for cluster catalog zone if it does not exist\n            {\n                IReadOnlyDictionary<string, TsigKey> existingKeys = _dnsWebService.DnsServer.TsigKeys;\n                if (existingKeys is null)\n                {\n                    Dictionary<string, TsigKey> updatedKeys = new Dictionary<string, TsigKey>();\n                    updatedKeys[clusterCatalogDomain] = new TsigKey(clusterCatalogDomain, TsigAlgorithm.HMAC_SHA256);\n\n                    _dnsWebService.DnsServer.TsigKeys = updatedKeys;\n                }\n                else if (!existingKeys.ContainsKey(clusterCatalogDomain))\n                {\n                    Dictionary<string, TsigKey> updatedKeys = new Dictionary<string, TsigKey>(existingKeys);\n                    updatedKeys[clusterCatalogDomain] = new TsigKey(clusterCatalogDomain, TsigAlgorithm.HMAC_SHA256);\n\n                    _dnsWebService.DnsServer.TsigKeys = updatedKeys;\n                }\n            }\n\n            //create cluster API token if it does not exist\n            {\n                List<UserSession> userSessions = _dnsWebService.AuthManager.GetSessions(session.User);\n                bool apiTokenExists = false;\n\n                foreach (UserSession existingSession in userSessions)\n                {\n                    if ((existingSession.Type == UserSessionType.ApiToken) && (existingSession.TokenName == clusterZoneInfo.Name))\n                    {\n                        apiTokenExists = true;\n                        break;\n                    }\n                }\n\n                if (!apiTokenExists)\n                    _dnsWebService.AuthManager.CreateApiToken(clusterZoneInfo.Name, session.User.Username, session.LastSeenRemoteAddress, session.LastSeenUserAgent);\n            }\n\n            //dispose existing nodes, if any\n            DisposeAllNodes();\n\n            //initialize cluster\n            _clusterNodes = new Dictionary<int, ClusterNode>(1)\n            {\n                [selfPrimaryNode.Id] = selfPrimaryNode\n            };\n\n            _clusterDomain = clusterZoneInfo.Name;\n            _heartbeatRefreshIntervalSeconds = HEARTBEAT_REFRESH_INTERVAL_SECONDS;\n            _heartbeatRetryIntervalSeconds = HEARTBEAT_RETRY_INTERVAL_SECONDS;\n            _configRefreshIntervalSeconds = CONFIG_REFRESH_INTERVAL_SECONDS;\n            _configRetryIntervalSeconds = CONFIG_RETRY_INTERVAL_SECONDS;\n\n            //update cluster primary zone and save zone file\n            FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values\n            RemoveAllClusterPrimaryZoneNSRecords(); //remove all existing NS records\n            AddClusterPrimaryZoneRecordsFor(selfPrimaryNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);\n\n            //update cluster catalog zone ACLs, TSIG key name and save zone file\n            UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo);\n\n            //finalize\n            _dnsWebService.DnsServer.ServerDomain = selfPrimaryNode.Name;\n\n            //save all changes\n            _dnsWebService.DnsServer.SaveConfigFile();\n            _dnsWebService.AuthManager.SaveConfigFile();\n            SaveConfigFile();\n        }\n\n        public void DeleteCluster(bool forceDelete)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to delete Cluster: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to delete Cluster: only a Primary node can delete the Cluster.\");\n\n            if (!forceDelete && (_clusterNodes.Count > 1))\n                throw new DnsServerException(\"Failed to delete Cluster: please remove all Secondary nodes before deleting the Cluster.\");\n\n            DeleteAllClusterConfig();\n        }\n\n        public ClusterNode JoinCluster(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyList<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to add Secondary node: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to add Secondary node: only a Primary node can add a Secondary node to the Cluster.\");\n\n            string secondaryNodeDomain = secondaryNodeUrl.Host.ToLowerInvariant();\n\n            if (!secondaryNodeDomain.EndsWith(\".\" + _clusterDomain, StringComparison.OrdinalIgnoreCase))\n                throw new DnsServerException(\"Failed to add Secondary node: the Secondary node domain name must be a subdomain of the Cluster domain name.\");\n\n            IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;\n\n            //validate for duplicate names\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n            {\n                if (existingClusterNode.Value.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase))\n                    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.\");\n            }\n\n            //add secondary node to cluster nodes\n            ClusterNode secondaryNode = new ClusterNode(this, secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Unknown);\n            Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count + 1);\n\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n                updatedClusterNodes[existingClusterNode.Value.Id] = existingClusterNode.Value;\n\n            if (!updatedClusterNodes.TryAdd(secondaryNode.Id, secondaryNode))\n                throw new DnsServerException(\"Failed to add Secondary node: node ID already exists in the Cluster. Please try again.\");\n\n            if (updatedClusterNodes.Count > 255)\n                throw new DnsServerException(\"Failed to add Secondary node: a maximum of 255 nodes are supported by the Cluster.\");\n\n            IReadOnlyDictionary<int, ClusterNode> originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes);\n            if (!ReferenceEquals(originalValue, existingClusterNodes))\n                throw new DnsServerException(\"Failed to add Secondary node: please try again.\");\n\n            secondaryNode.InitializeHeartbeatTimer();\n\n            //update cluster zone and save zone file\n            FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values\n            AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate);\n\n            //update cluster catalog zone ACLs and save zone file\n            UpdateClusterCatalogZoneOptions();\n\n            //save all changes\n            SaveConfigFile();\n\n            //notify all secondary nodes\n            TriggerNotifyAllSecondaryNodes();\n\n            //trigger NS and SOA update for member zones\n            TriggerRecordUpdateForClusterCatalogMemberZones();\n\n            return secondaryNode;\n        }\n\n        public async Task<ClusterNode> AskSecondaryNodeToLeaveClusterAsync(int secondaryNodeId)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to ask Secondary node to leave: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to ask Secondary node to leave: only a Primary node can ask a Secondary node to leave the Cluster.\");\n\n            //find existing secondary node\n            if (!_clusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode))\n                throw new DnsServerException(\"Failed to ask Secondary node to leave: the specified node does not exist in the Cluster.\");\n\n            if (secondaryNode.Type == ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to ask Secondary node to leave: the specified node is the Cluster Primary node and cannot be removed.\");\n\n            //ask secondary node to leave the cluster\n            await secondaryNode.AskSecondaryNodeToLeaveClusterAsync();\n\n            return secondaryNode;\n        }\n\n        public ClusterNode DeleteSecondaryNode(int secondaryNodeId)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to delete Secondary node: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to delete Secondary node: only a Primary node can delete a Secondary node from the Cluster.\");\n\n            //find existing secondary node\n            IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;\n\n            if (!existingClusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode))\n                throw new DnsServerException(\"Failed to delete Secondary node: the specified node does not exist in the Cluster.\");\n\n            if (secondaryNode.Type == ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to delete Secondary node: the specified node is the Cluster Primary node and cannot be deleted.\");\n\n            //delete secondary node from cluster nodes\n            Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count - 1);\n\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n            {\n                if (existingClusterNode.Key == secondaryNodeId)\n                    continue;\n\n                updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;\n            }\n\n            IReadOnlyDictionary<int, ClusterNode> originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes);\n            if (!ReferenceEquals(originalValue, existingClusterNodes))\n                throw new InvalidOperationException(\"Failed to delete Secondary node: please try again.\");\n\n            secondaryNode.Dispose();\n\n            //update cluster zone and save zone file\n            RemoveClusterPrimaryZoneRecordsFor(secondaryNode);\n\n            //update cluster catalog zone ACLs and save zone file\n            UpdateClusterCatalogZoneOptions();\n\n            //save all changes\n            SaveConfigFile();\n\n            //notify all secondary nodes\n            TriggerNotifyAllSecondaryNodes();\n\n            //trigger NS and SOA update for member zones\n            TriggerRecordUpdateForClusterCatalogMemberZones();\n\n            return secondaryNode;\n        }\n\n        public ClusterNode UpdateSecondaryNode(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyList<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to update Secondary node: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to update Secondary node: only a Primary node can update a Secondary node's details in the Cluster.\");\n\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n\n            if (!clusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode))\n                throw new DnsServerException(\"Failed to update Secondary node: the specified node does not exist in the Cluster.\");\n\n            if (secondaryNode.Type != ClusterNodeType.Secondary)\n                throw new DnsServerException(\"Failed to update Secondary node: the specified node to update must be a Secondary node.\");\n\n            //validate for duplicate names\n            foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n            {\n                if (clusterNode.Key == secondaryNodeId)\n                    continue; //skip self\n\n                if (clusterNode.Value.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase))\n                    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.\");\n            }\n\n            bool secondaryNodeDomainChanged = !secondaryNode.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase);\n\n            //find existing record TTL values\n            FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);\n\n            //update cluster zone to remove existing records for secondary node\n            RemoveClusterPrimaryZoneRecordsFor(secondaryNode);\n\n            //update secondary node URL and IP address\n            secondaryNode.UpdateNode(secondaryNodeUrl, secondaryNodeIpAddresses);\n\n            //update cluster zone to add updated records for secondary node and save zone file\n            AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate);\n\n            //update cluster catalog zone ACLs and save zone file\n            UpdateClusterCatalogZoneOptions();\n\n            //save all changes\n            SaveConfigFile();\n\n            //notify all secondary nodes\n            TriggerNotifyAllSecondaryNodes();\n\n            //trigger NS and SOA update for member zones only if secondary node domain name has changed\n            if (secondaryNodeDomainChanged)\n                TriggerRecordUpdateForClusterCatalogMemberZones();\n\n            return secondaryNode;\n        }\n\n        public Task TransferConfigAsync(Stream zipStream, DateTime ifModifiedSince, IReadOnlyCollection<string> includeZones)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to transfer configuration: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to transfer configuration: only the Primary node can transfer the configuration.\");\n\n            return _dnsWebService.BackupConfigAsync(zipStream: zipStream,\n                                                    authConfig: true,\n                                                    clusterConfig: false,\n                                                    webServiceSettings: false,\n                                                    dnsSettings: true,\n                                                    logSettings: false,\n                                                    zones: true,\n                                                    allowedZones: true,\n                                                    blockedZones: true,\n                                                    blockLists: true,\n                                                    apps: true,\n                                                    scopes: false,\n                                                    stats: false,\n                                                    logs: false,\n                                                    isConfigTransfer: true,\n                                                    ifModifiedSince: ifModifiedSince,\n                                                    includeZones: includeZones);\n        }\n\n        public void UpdateClusterOptions(ushort heartbeatRefreshIntervalSeconds, ushort heartbeatRetryIntervalSeconds, ushort configRefreshIntervalSeconds, ushort configRetryIntervalSeconds)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to update Cluster options: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to update Cluster options: only the Primary node can update the Cluster options.\");\n\n            if ((heartbeatRefreshIntervalSeconds < 10) || (heartbeatRefreshIntervalSeconds > 300))\n                throw new ArgumentOutOfRangeException(nameof(heartbeatRefreshIntervalSeconds));\n\n            if ((heartbeatRetryIntervalSeconds < 10) || (heartbeatRetryIntervalSeconds > 300))\n                throw new ArgumentOutOfRangeException(nameof(heartbeatRetryIntervalSeconds));\n\n            if ((configRefreshIntervalSeconds < 30) || (configRefreshIntervalSeconds > 3600))\n                throw new ArgumentOutOfRangeException(nameof(configRefreshIntervalSeconds));\n\n            if ((configRetryIntervalSeconds < 30) || (configRetryIntervalSeconds > 3600))\n                throw new ArgumentOutOfRangeException(nameof(configRetryIntervalSeconds));\n\n            if (configRefreshIntervalSeconds <= heartbeatRefreshIntervalSeconds)\n                throw new ArgumentException(\"Failed to update Cluster options: The config refresh interval must be greater than the heartbeat refresh interval.\");\n\n            bool changed = false;\n\n            if (_heartbeatRefreshIntervalSeconds != heartbeatRefreshIntervalSeconds)\n            {\n                _heartbeatRefreshIntervalSeconds = heartbeatRefreshIntervalSeconds;\n                changed = true;\n            }\n\n            if (_heartbeatRetryIntervalSeconds != heartbeatRetryIntervalSeconds)\n            {\n                _heartbeatRetryIntervalSeconds = heartbeatRetryIntervalSeconds;\n                changed = true;\n            }\n\n            if (_configRefreshIntervalSeconds != configRefreshIntervalSeconds)\n            {\n                _configRefreshIntervalSeconds = configRefreshIntervalSeconds;\n                changed = true;\n            }\n\n            if (_configRetryIntervalSeconds != configRetryIntervalSeconds)\n            {\n                _configRetryIntervalSeconds = configRetryIntervalSeconds;\n                changed = true;\n            }\n\n            if (changed)\n            {\n                //apply new interval to all cluster nodes immediately\n                UpdateHeartbeatTimerForAllClusterNodes();\n\n                //save changes\n                SaveConfigFile();\n\n                //trigger notify to all secondary nodes\n                TriggerNotifyAllSecondaryNodes();\n            }\n        }\n\n        private void FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl)\n        {\n            //try get existing NS record TTL\n            IReadOnlyList<DnsResourceRecord> existingNSRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS);\n            if (existingNSRecords.Count > 0)\n            {\n                DnsResourceRecord existingNSRecord = existingNSRecords[0];\n\n                nsTtl = existingNSRecord.TTL;\n\n                string nsDomain = (existingNSRecord.RDATA as DnsNSRecordData).NameServer;\n\n                if (nsDomain.EndsWith(\".\" + _clusterDomain, StringComparison.OrdinalIgnoreCase))\n                {\n                    IReadOnlyList<DnsResourceRecord> existingRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, nsDomain, DnsResourceRecordType.A);\n                    if (existingRecords.Count == 0)\n                        existingRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, nsDomain, DnsResourceRecordType.AAAA);\n\n                    if (existingRecords.Count > 0)\n                        aTtl = existingRecords[0].TTL;\n                    else\n                        aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl;\n                }\n                else\n                {\n                    aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl;\n                }\n            }\n            else\n            {\n                nsTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultNsRecordTtl;\n                aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl;\n            }\n        }\n\n        private void RemoveAllClusterPrimaryZoneNSRecords()\n        {\n            //remove all existing NS records\n            _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS);\n        }\n\n        private void RemoveClusterPrimaryZoneRecordsFor(ClusterNode node)\n        {\n            //remove NS record\n            _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS, new DnsNSRecordData(node.Name));\n\n            //remove A/AAAA records\n            _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, node.Name, DnsResourceRecordType.A);\n            _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, node.Name, DnsResourceRecordType.AAAA);\n\n            //remove PTR record\n            foreach (IPAddress ipAddress in node.IPAddresses)\n            {\n                string ptrDomain = Zone.GetReverseZone(ipAddress, ipAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);\n                _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(ptrDomain, ptrDomain, DnsResourceRecordType.PTR, new DnsPTRRecordData(node.Name));\n            }\n\n            //remove TLSA DANE-EE record\n            _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, $\"_{node.Url.Port}._tcp.{node.Name}\", DnsResourceRecordType.TLSA);\n\n            //save zone file\n            _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(_clusterDomain);\n        }\n\n        private void AddClusterPrimaryZoneRecordsFor(ClusterNode node, uint nsTtl, uint aTtl, X509Certificate2 certificate)\n        {\n            const string recordComments = \"Cluster managed record. Do not update or delete.\";\n\n            if (node.Type == ClusterNodeType.Primary)\n            {\n                //update SOA record\n                IReadOnlyList<DnsResourceRecord> existingSoaRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.SOA);\n                DnsResourceRecord existingSoaRecord = existingSoaRecords[0];\n                DnsSOARecordData existingSoa = existingSoaRecord.RDATA as DnsSOARecordData;\n\n                DnsSOARecordData newSoa = new DnsSOARecordData(node.Name, existingSoa.ResponsiblePerson, existingSoa.Serial, existingSoa.Refresh, existingSoa.Retry, existingSoa.Expire, existingSoa.Minimum);\n                DnsResourceRecord newSoaRecord = new DnsResourceRecord(_clusterDomain, DnsResourceRecordType.SOA, DnsClass.IN, existingSoaRecord.TTL, newSoa);\n\n                _dnsWebService.DnsServer.AuthZoneManager.SetRecord(_clusterDomain, newSoaRecord);\n            }\n\n            //add NS record\n            DnsResourceRecord nsRecord = new DnsResourceRecord(_clusterDomain, DnsResourceRecordType.NS, DnsClass.IN, nsTtl, new DnsNSRecordData(node.Name));\n\n            GenericRecordInfo nsRecordInfo = nsRecord.GetAuthGenericRecordInfo();\n            nsRecordInfo.LastModified = DateTime.UtcNow;\n            nsRecordInfo.Comments = recordComments;\n\n            _dnsWebService.DnsServer.AuthZoneManager.AddRecord(_clusterDomain, nsRecord);\n\n            //set A/AAAA record\n            List<DnsResourceRecord> ipv4AddressRecords = new List<DnsResourceRecord>(node.IPAddresses.Count);\n            List<DnsResourceRecord> ipv6AddressRecords = new List<DnsResourceRecord>(node.IPAddresses.Count);\n\n            foreach (IPAddress ipAddress in node.IPAddresses)\n            {\n                DnsResourceRecord record;\n\n                switch (ipAddress.AddressFamily)\n                {\n                    case AddressFamily.InterNetwork:\n                        record = new DnsResourceRecord(node.Name, DnsResourceRecordType.A, DnsClass.IN, aTtl, new DnsARecordData(ipAddress));\n                        ipv4AddressRecords.Add(record);\n                        break;\n\n                    case AddressFamily.InterNetworkV6:\n                        record = new DnsResourceRecord(node.Name, DnsResourceRecordType.AAAA, DnsClass.IN, aTtl, new DnsAAAARecordData(ipAddress));\n                        ipv6AddressRecords.Add(record);\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n\n                GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();\n                recordInfo.LastModified = DateTime.UtcNow;\n                recordInfo.Comments = recordComments;\n            }\n\n            if (ipv4AddressRecords.Count > 0)\n                _dnsWebService.DnsServer.AuthZoneManager.SetRecords(_clusterDomain, ipv4AddressRecords);\n\n            if (ipv6AddressRecords.Count > 0)\n                _dnsWebService.DnsServer.AuthZoneManager.SetRecords(_clusterDomain, ipv6AddressRecords);\n\n            //set PTR record\n            foreach (IPAddress ipAddress in node.IPAddresses)\n            {\n                string ptrDomain = Zone.GetReverseZone(ipAddress, ipAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);\n\n                AuthZoneInfo reverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain);\n                if (reverseZoneInfo is not null)\n                {\n                    if (!reverseZoneInfo.Internal && (reverseZoneInfo.Type == AuthZoneType.Primary))\n                    {\n                        DnsResourceRecord ptrRecord = new DnsResourceRecord(ptrDomain, DnsResourceRecordType.PTR, DnsClass.IN, aTtl, new DnsPTRRecordData(node.Name));\n\n                        GenericRecordInfo ptrRecordInfo = ptrRecord.GetAuthGenericRecordInfo();\n                        ptrRecordInfo.LastModified = DateTime.UtcNow;\n                        ptrRecordInfo.Comments = recordComments;\n\n                        _dnsWebService.DnsServer.AuthZoneManager.SetRecord(reverseZoneInfo.Name, ptrRecord);\n                    }\n                }\n            }\n\n            //set TLSA DANE-EE record\n            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));\n\n            GenericRecordInfo tlsaRecordInfo = tlsaRecord.GetAuthGenericRecordInfo();\n            tlsaRecordInfo.LastModified = DateTime.UtcNow;\n            tlsaRecordInfo.Comments = recordComments;\n\n            _dnsWebService.DnsServer.AuthZoneManager.SetRecord(_clusterDomain, tlsaRecord);\n\n            //save zone file\n            _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(_clusterDomain);\n        }\n\n        public void UpdateClusterRecordsFor(AuthZoneInfo zoneInfo)\n        {\n            if (zoneInfo.Type != AuthZoneType.Primary)\n                throw new InvalidOperationException();\n\n            //set NS records for cluster\n            IReadOnlyList<DnsResourceRecord> existingNSRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.NS);\n            uint ttl;\n\n            if (existingNSRecords.Count > 0)\n                ttl = existingNSRecords[0].TTL;\n            else\n                ttl = _dnsWebService.DnsServer.AuthZoneManager.DefaultNsRecordTtl;\n\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n            DnsResourceRecord[] nsRecords = new DnsResourceRecord[clusterNodes.Count];\n            int i = 0;\n\n            foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n                nsRecords[i++] = new DnsResourceRecord(zoneInfo.Name, DnsResourceRecordType.NS, DnsClass.IN, ttl, new DnsNSRecordData(clusterNode.Value.Name));\n\n            //set NS record\n            _dnsWebService.DnsServer.AuthZoneManager.SetRecords(zoneInfo.Name, nsRecords);\n\n            //ensure correct SOA primary name server\n            IReadOnlyList<DnsResourceRecord> existingSoaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);\n            if (existingSoaRecords.Count > 0)\n            {\n                DnsResourceRecord existingSoaRecord = existingSoaRecords[0];\n                DnsSOARecordData existingSoa = existingSoaRecord.RDATA as DnsSOARecordData;\n\n                //set SOA record\n                _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))]);\n            }\n        }\n\n        private void TriggerRecordUpdateForClusterCatalogMemberZones()\n        {\n            int id = RandomNumberGenerator.GetInt32(int.MaxValue);\n            _recordUpdateForMemberZonesId = id;\n\n            ThreadPool.QueueUserWorkItem(delegate (object state)\n            {\n                try\n                {\n                    //get cluster catalog zone info\n                    AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(\"cluster-catalog.\" + _clusterDomain);\n                    if ((clusterCatalogZoneInfo is null) || (clusterCatalogZoneInfo.Type != AuthZoneType.Catalog))\n                        throw new InvalidOperationException();\n\n                    //get all member zone names for cluster catalog zone\n                    IReadOnlyCollection<string> memberZoneNames = (clusterCatalogZoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames();\n\n                    foreach (string memberZoneName in memberZoneNames)\n                    {\n                        if (_recordUpdateForMemberZonesId != id)\n                            return; //stop current update since another update has been triggered\n\n                        //get member zone info\n                        AuthZoneInfo memberZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName);\n                        if ((memberZoneInfo is null) || (memberZoneInfo.Type != AuthZoneType.Primary))\n                            continue; //process is only for primary zones\n\n                        //update NS and SOA records for the member zone\n                        UpdateClusterRecordsFor(memberZoneInfo);\n\n                        //save zone file\n                        _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(memberZoneName);\n                    }\n\n                    _dnsWebService.LogManager.Write(\"The Cluster Catalog member zones NS and SOA records were successfully updated to reflect the Cluster changes.\");\n                }\n                catch (Exception ex)\n                {\n                    _dnsWebService.LogManager.Write(ex);\n                }\n            });\n        }\n\n        private void UpdateClusterCatalogZoneOptions()\n        {\n            string clusterCatalogDomain = \"cluster-catalog.\" + _clusterDomain;\n\n            AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);\n            if (clusterCatalogZoneInfo is null)\n                throw new InvalidOperationException();\n\n            UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo);\n        }\n\n        private void UpdateClusterCatalogZoneOptions(AuthZoneInfo clusterCatalogZoneInfo)\n        {\n            //set cluster catalog zone options for Zone Transfer ACLs and notify addresses\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n\n            List<NetworkAccessControl> zoneTransferACL = new List<NetworkAccessControl>(clusterNodes.Count * 2);\n            List<IPAddress> notifyNameServers = new List<IPAddress>(clusterNodes.Count * 2);\n\n            foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n            {\n                if (clusterNode.Value.Type == ClusterNodeType.Primary)\n                    continue;\n\n                foreach (IPAddress ipAddress in clusterNode.Value.IPAddresses)\n                    zoneTransferACL.Add(new NetworkAccessControl(ipAddress, 32));\n\n                notifyNameServers.AddRange(clusterNode.Value.IPAddresses);\n            }\n\n            clusterCatalogZoneInfo.ZoneTransferNetworkACL = zoneTransferACL;\n            clusterCatalogZoneInfo.NotifyNameServers = notifyNameServers;\n\n            clusterCatalogZoneInfo.ZoneTransfer = AuthZoneTransfer.UseSpecifiedNetworkACL;\n            clusterCatalogZoneInfo.Notify = AuthZoneNotify.SpecifiedNameServers;\n\n            //set cluster catalog zone options for zone transfer TSIG key names\n            IReadOnlySet<string> existingKeyNames = clusterCatalogZoneInfo.ZoneTransferTsigKeyNames;\n            if (existingKeyNames is null)\n            {\n                HashSet<string> updatedKeyNames = [clusterCatalogZoneInfo.Name];\n\n                clusterCatalogZoneInfo.ZoneTransferTsigKeyNames = updatedKeyNames;\n            }\n            else if (!existingKeyNames.Contains(clusterCatalogZoneInfo.Name))\n            {\n                HashSet<string> updatedKeyNames = [.. existingKeyNames, clusterCatalogZoneInfo.Name];\n\n                clusterCatalogZoneInfo.ZoneTransferTsigKeyNames = updatedKeyNames;\n            }\n\n            _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(clusterCatalogZoneInfo.Name);\n        }\n\n        public void TriggerNotifyAllSecondaryNodesIfPrimarySelfNode()\n        {\n            if (GetSelfNode().Type == ClusterNodeType.Primary)\n                TriggerNotifyAllSecondaryNodes();\n        }\n\n        public void TriggerNotifyAllSecondaryNodes(int notifyInterval = NOTIFY_ALL_SECONDARY_NODES_TIMER_INTERVAL)\n        {\n            if (_notifyAllSecondaryNodesTimerTriggered)\n                return;\n\n            _notifyAllSecondaryNodesTimer.Change(notifyInterval, Timeout.Infinite);\n            _notifyAllSecondaryNodesTimerTriggered = true;\n        }\n\n        private async void NotifyAllSecondaryNodesTimerCallbackAsync(object state)\n        {\n            try\n            {\n                IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n                ClusterNode primaryNode = null;\n\n                foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n                {\n                    if (clusterNode.Value.Type == ClusterNodeType.Primary)\n                    {\n                        primaryNode = clusterNode.Value;\n                        break;\n                    }\n                }\n\n                if ((primaryNode is null) || (primaryNode.State != ClusterNodeState.Self))\n                    throw new InvalidOperationException();\n\n                List<Task> tasks = new List<Task>(clusterNodes.Count);\n\n                foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n                {\n                    if (clusterNode.Value.Type == ClusterNodeType.Primary)\n                        continue; //skip primary cluster node\n\n                    tasks.Add(clusterNode.Value.NotifySecondaryNodeAsync(primaryNode));\n                }\n\n                await Task.WhenAll(tasks); //notify node call does error logging\n            }\n            catch (Exception ex)\n            {\n                _dnsWebService.LogManager.Write(ex);\n            }\n            finally\n            {\n                _notifyAllSecondaryNodesTimerTriggered = false;\n            }\n        }\n\n        #endregion\n\n        #region secondary node\n\n        public async Task InitializeAndJoinClusterAsync(IReadOnlyList<IPAddress> secondaryNodeIpAddresses, Uri primaryNodeUrl, string primaryNodeUsername, string primaryNodePassword, string primaryNodeTotp = null, IReadOnlyList<IPAddress> primaryNodeIpAddresses = null, bool ignoreCertificateErrors = false, CancellationToken cancellationToken = default)\n        {\n            if (ClusterInitialized)\n                throw new DnsServerException(\"Failed to join Cluster: the Cluster is already initialized.\");\n\n            if (!_dnsWebService.IsWebServiceTlsEnabled)\n                throw new InvalidOperationException();\n\n            if (primaryNodeIpAddresses is null)\n            {\n                try\n                {\n                    IReadOnlyList<IPAddress> ipAddresses = await DnsClient.ResolveIPAsync(_dnsWebService.DnsServer, primaryNodeUrl.Host, _dnsWebService.DnsServer.PreferIPv6, cancellationToken);\n                    if (ipAddresses.Count < 1)\n                        throw new DnsServerException($\"The domain name '{primaryNodeUrl.Host}' does not have an A/AAAA record configured.\");\n\n                    primaryNodeIpAddresses = ipAddresses;\n                }\n                catch (Exception ex)\n                {\n                    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);\n                }\n            }\n\n            //login to primary node API\n            using HttpApiClient primaryNodeApiClient = new HttpApiClient(primaryNodeUrl, _dnsWebService.DnsServer.Proxy, _dnsWebService.DnsServer.PreferIPv6, ignoreCertificateErrors, new InternalDnsClient(_dnsWebService.DnsServer, primaryNodeIpAddresses));\n\n            try\n            {\n                _ = await primaryNodeApiClient.LoginAsync(primaryNodeUsername, primaryNodePassword, primaryNodeTotp, false, cancellationToken);\n            }\n            catch (TwoFactorAuthRequiredHttpApiClientException ex)\n            {\n                throw new TwoFactorAuthRequiredWebServiceException(\"Failed to join Cluster: two-factor authentication is required by the Primary node user account.\", ex);\n            }\n\n            try\n            {\n                //get cluster info\n                ClusterInfo primaryNodeClusterInfo = await primaryNodeApiClient.GetClusterStateAsync(cancellationToken: cancellationToken);\n\n                //do validations\n                if (!primaryNodeClusterInfo.ClusterInitialized)\n                    throw new DnsServerException(\"Failed to join Cluster: the Primary node does not have a Cluster initialized.\");\n\n                string clusterCatalogDomain = \"cluster-catalog.\" + primaryNodeClusterInfo.ClusterDomain;\n\n                if (_dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain) is not null)\n                    throw new DnsServerException($\"Failed to join Cluster: the zone '{clusterCatalogDomain}' already exists. Please delete the '{clusterCatalogDomain}' zone and try again.\");\n\n                if (_dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(primaryNodeClusterInfo.ClusterDomain) is not null)\n                    throw new DnsServerException($\"Failed to join Cluster: the zone '{primaryNodeClusterInfo.ClusterDomain}' already exists. Please delete the '{primaryNodeClusterInfo.ClusterDomain}' zone and try again.\");\n\n                //create self node\n                string serverDomain = _dnsWebService.DnsServer.ServerDomain;\n                if (!serverDomain.EndsWith(\".\" + primaryNodeClusterInfo.ClusterDomain, StringComparison.OrdinalIgnoreCase))\n                {\n                    int x = serverDomain.IndexOf('.');\n                    if (x < 0)\n                        serverDomain = serverDomain + \".\" + primaryNodeClusterInfo.ClusterDomain;\n                    else\n                        serverDomain = string.Concat(serverDomain.AsSpan(0, x), \".\", primaryNodeClusterInfo.ClusterDomain);\n                }\n\n                Uri secondaryNodeUrl = new Uri($\"https://{serverDomain}:{_dnsWebService.WebServiceTlsPort}/\");\n\n                ClusterNode selfSecondaryNode = new ClusterNode(this, RandomNumberGenerator.GetInt32(int.MaxValue), secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Self);\n\n                //join cluster\n                primaryNodeClusterInfo = await primaryNodeApiClient.JoinClusterAsync(selfSecondaryNode.Id, secondaryNodeUrl, secondaryNodeIpAddresses, _dnsWebService.WebServiceTlsCertificate, cancellationToken);\n\n                //initialize cluster\n                Dictionary<int, ClusterNode> clusterNodes = new Dictionary<int, ClusterNode>(primaryNodeClusterInfo.ClusterNodes.Count + 1);\n\n                clusterNodes[selfSecondaryNode.Id] = selfSecondaryNode;\n\n                foreach (ClusterInfo.ClusterNodeInfo nodeInfo in primaryNodeClusterInfo.ClusterNodes)\n                {\n                    if (nodeInfo.Id == selfSecondaryNode.Id)\n                        continue; //skip self node\n\n                    ClusterNode node = new ClusterNode(this, nodeInfo);\n                    clusterNodes[node.Id] = node;\n                }\n\n                DisposeAllNodes(); //dispose existing nodes, if any\n\n                _clusterNodes = clusterNodes;\n\n                _clusterDomain = primaryNodeClusterInfo.ClusterDomain;\n                _heartbeatRefreshIntervalSeconds = primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds;\n                _heartbeatRetryIntervalSeconds = primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds;\n                _configRefreshIntervalSeconds = primaryNodeClusterInfo.ConfigRefreshIntervalSeconds;\n                _configRetryIntervalSeconds = primaryNodeClusterInfo.ConfigRetryIntervalSeconds;\n\n                try\n                {\n                    //sync entire config from primary node first to get TSIG keys for secondary catalog zone transfer\n                    _configLastSynced = DateTime.UnixEpoch; //reset last sync time to ensure full sync\n                    await SyncConfigFromAsync(primaryNodeApiClient, cancellationToken: cancellationToken);\n\n                    //create cluster secondary catalog zone\n                    AuthZoneInfo clusterSecondaryCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateSecondaryCatalogZone(clusterCatalogDomain, primaryNodeIpAddresses.Convert(delegate (IPAddress ipAddress) { return new NameServerAddress(primaryNodeUrl.Host, ipAddress); }), DnsTransportProtocol.Tcp, clusterCatalogDomain);\n                    if (clusterSecondaryCatalogZoneInfo is null)\n                        throw new DnsServerException($\"Failed to join Cluster: the zone '{clusterCatalogDomain}' already exists. Please delete the '{clusterCatalogDomain}' zone and try again.\");\n\n                    //set cluster secondary catalog zone permissions\n                    _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterSecondaryCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                    _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterSecondaryCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View);\n                }\n                catch\n                {\n                    try\n                    {\n                        await primaryNodeApiClient.DeleteSecondaryNodeAsync(selfSecondaryNode.Id, cancellationToken);\n                    }\n                    catch\n                    { }\n\n                    DeleteAllClusterConfig();\n                    throw;\n                }\n\n                //initialize heartbeat timer for all nodes here since config sync and zone transfers needs to occur first for DANE validation to work\n                InitializeHeartbeatTimerFor(clusterNodes);\n\n                //start config refresh timer to refresh as per config refresh interval as config was just synced\n                UpdateConfigRefreshTimer(_configRefreshIntervalSeconds * 1000);\n\n                //finalize\n                _dnsWebService.DnsServer.ServerDomain = selfSecondaryNode.Name;\n\n                //save all changes\n                _dnsWebService.DnsServer.SaveConfigFile();\n                _dnsWebService.AuthManager.SaveConfigFile();\n                SaveConfigFile();\n            }\n            finally\n            {\n                try\n                {\n                    //logout from primary node\n                    await primaryNodeApiClient.LogoutAsync(cancellationToken);\n                }\n                catch\n                { }\n            }\n        }\n\n        public async Task LeaveClusterAsync(bool forceLeave)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to leave Cluster: the Cluster is not initialized.\");\n\n            ClusterNode primaryNode = GetPrimaryNode();\n\n            if (primaryNode.State == ClusterNodeState.Self)\n                throw new DnsServerException(\"Failed to leave Cluster: a Primary self node cannot leave the Cluster.\");\n\n            ClusterNode secondaryNode = GetSelfNode();\n\n            if (secondaryNode.Type != ClusterNodeType.Secondary)\n                throw new DnsServerException(\"Failed to leave Cluster: only Secondary nodes can leave the Cluster.\");\n\n            if (!forceLeave)\n            {\n                //delete self node from cluster on primary node\n                await primaryNode.DeleteSecondaryNodeAsync(secondaryNode);\n            }\n\n            //delete all cluster config\n            DeleteAllClusterConfig();\n        }\n\n        public async Task<ClusterNode> UpdatePrimaryNodeAsync(Uri primaryNodeUrl, IReadOnlyList<IPAddress> primaryNodeIpAddresses = null, int primaryNodeId = -1, CancellationToken cancellationToken = default)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to update Primary node: the Cluster is not initialized.\");\n\n            if (primaryNodeIpAddresses is null)\n            {\n                try\n                {\n                    IReadOnlyList<IPAddress> ipAddresses = await DnsClient.ResolveIPAsync(_dnsWebService.DnsServer, primaryNodeUrl.Host, _dnsWebService.DnsServer.PreferIPv6, cancellationToken);\n                    if (ipAddresses.Count < 1)\n                        throw new DnsServerException($\"The domain name '{primaryNodeUrl.Host}' does not have an A/AAAA record configured.\");\n\n                    primaryNodeIpAddresses = ipAddresses;\n                }\n                catch (Exception ex)\n                {\n                    throw new DnsServerException($\"Failed to update Primary node: the Primary node domain name '{primaryNodeUrl.Host}' could not be resolved to an IP address.\", ex);\n                }\n            }\n\n            ClusterNode primaryNode;\n\n            if (primaryNodeId < 0)\n                primaryNode = GetPrimaryNode();\n            else if (!_clusterNodes.TryGetValue(primaryNodeId, out primaryNode))\n                throw new DnsServerException(\"Failed to update Primary node: the specified Primary node ID does not exists in the Cluster.\");\n\n            if (primaryNode.State == ClusterNodeState.Self)\n                throw new DnsServerException(\"Failed to update Primary node: the specified node is the self node and cannot be updated this way.\");\n\n            if (primaryNode.Type == ClusterNodeType.Secondary)\n            {\n                //secondary node was promoted to primary node\n                ClusterNode formerPrimaryNode = GetPrimaryNode();\n\n                //dispose former primary node immediately to stop heartbeat\n                formerPrimaryNode.Dispose();\n\n                //remove former primary node from cluster nodes\n                IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;\n                Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count - 1);\n\n                foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n                {\n                    if (existingClusterNode.Key == formerPrimaryNode.Id)\n                        continue;\n\n                    updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;\n                }\n\n                //update cluster nodes\n                _clusterNodes = updatedClusterNodes;\n\n                //promote secondary node to primary immediately\n                primaryNode.PromoteToPrimaryNode();\n\n                //ensure to save changes\n                SaveConfigFile();\n            }\n\n            //validate for duplicate names\n            foreach (KeyValuePair<int, ClusterNode> clusterNode in _clusterNodes)\n            {\n                if (clusterNode.Key == primaryNode.Id)\n                    continue; //skip self\n\n                if (clusterNode.Value.Name.Equals(primaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase))\n                    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.\");\n            }\n\n            //get cluster secondary catalog zone\n            string clusterCatalogDomain = \"cluster-catalog.\" + _clusterDomain;\n\n            AuthZoneInfo clusterSecondaryCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);\n            if (clusterSecondaryCatalogZoneInfo is null)\n                throw new DnsServerException($\"Failed to update Primary node: the Cluster Secondary Catalog zone '{clusterCatalogDomain}' does not exists.\");\n\n            //update primary node\n            primaryNode.UpdateNode(primaryNodeUrl, primaryNodeIpAddresses);\n\n            //update cluster catalog zone's primary name server\n            clusterSecondaryCatalogZoneInfo.PrimaryNameServerAddresses = primaryNodeIpAddresses.Convert(delegate (IPAddress ipAddress) { return new NameServerAddress(primaryNodeUrl.Host, ipAddress); });\n\n            //save all changes\n            _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(clusterSecondaryCatalogZoneInfo.Name);\n            SaveConfigFile();\n\n            //trigger config and zone refresh\n            TriggerRefreshForConfig(CONFIG_REFRESH_TIMER_INTERVAL);\n\n            return primaryNode;\n        }\n\n        public void TriggerRefreshForConfig(IReadOnlyCollection<string> configRefreshIncludeZones = null)\n        {\n            //do validation\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to refresh configuration: the Cluster is not initialized.\");\n\n            ClusterNode primaryNode = GetPrimaryNode();\n\n            if (primaryNode.State == ClusterNodeState.Self)\n                throw new DnsServerException(\"Failed to refresh configuration: only Secondary nodes can sync configuration from Primary nodes.\");\n\n            TriggerRefreshForConfig(CONFIG_REFRESH_TIMER_INTERVAL, configRefreshIncludeZones);\n        }\n\n        public void TriggerResyncForConfig()\n        {\n            //do validation\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to resync configuration: the Cluster is not initialized.\");\n\n            ClusterNode primaryNode = GetPrimaryNode();\n\n            if (primaryNode.State == ClusterNodeState.Self)\n                throw new DnsServerException(\"Failed to resync configuration: only Secondary nodes can sync configuration from Primary nodes.\");\n\n            _configLastSynced = DateTime.UnixEpoch; //to ensure complete config resync\n\n            //trigger immediate config refresh\n            TriggerRefreshForConfig(0);\n        }\n\n        private void TriggerRefreshForConfig(int refreshInterval, IReadOnlyCollection<string> configRefreshIncludeZones = null)\n        {\n            _configRefreshLock.Wait();\n            try\n            {\n                if (configRefreshIncludeZones is not null)\n                {\n                    if (_configRefreshIncludeZones is null)\n                        _configRefreshIncludeZones = configRefreshIncludeZones;\n                    else\n                        _configRefreshIncludeZones = [.. _configRefreshIncludeZones, .. configRefreshIncludeZones];\n                }\n\n                if (_configRefreshTimerTriggered)\n                    return;\n\n                _configRefreshTimer.Change(refreshInterval, Timeout.Infinite);\n                _configRefreshTimerTriggered = true;\n            }\n            finally\n            {\n                _configRefreshLock.Release();\n            }\n        }\n\n        private async void ConfigRefreshTimerCallbackAsync(object state)\n        {\n            bool success = false;\n\n            await _configRefreshLock.WaitAsync();\n            try\n            {\n                ClusterNode primaryNode = GetPrimaryNode();\n\n                if (primaryNode.State == ClusterNodeState.Self)\n                    throw new InvalidOperationException();\n\n                //update cluster options\n                UpdateClusterFromPrimaryNode(await primaryNode.GetClusterStateAsync());\n\n                //sync config from primary node\n                await primaryNode.SyncConfigAsync(_configRefreshIncludeZones);\n\n                success = true;\n            }\n            catch (Exception ex)\n            {\n                _dnsWebService.LogManager.Write(\"Failed to sync server configuration from the Primary node.\\r\\n\" + ex.ToString());\n            }\n            finally\n            {\n                if (success)\n                {\n                    _configRefreshTimerTriggered = false;\n                    _configRefreshIncludeZones = null;\n                }\n\n                try\n                {\n                    _configRefreshTimer.Change(success ? _configRefreshIntervalSeconds * 1000 : _configRetryIntervalSeconds * 1000, Timeout.Infinite);\n                }\n                catch (ObjectDisposedException)\n                { }\n\n                try\n                {\n                    _configRefreshLock.Release();\n                }\n                catch (ObjectDisposedException)\n                { }\n            }\n        }\n\n        public async Task SyncConfigFromAsync(HttpApiClient primaryNodeApiClient, IReadOnlyCollection<string> includeZones = null, CancellationToken cancellationToken = default)\n        {\n            string tmpFile = Path.GetTempFileName();\n            try\n            {\n                await using (FileStream configZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                {\n                    //get config from primary node\n                    (Stream, DateTime) response = await primaryNodeApiClient.TransferConfigFromPrimaryNodeAsync(_configLastSynced, includeZones, cancellationToken);\n\n                    await using (Stream stream = response.Item1)\n                    {\n                        await stream.CopyToAsync(configZipStream, cancellationToken);\n                    }\n\n                    //dynamically load config\n                    configZipStream.Position = 0;\n\n                    await _dnsWebService.RestoreConfigAsync(zipStream: configZipStream,\n                                                            authConfig: true,\n                                                            clusterConfig: false,\n                                                            webServiceSettings: false,\n                                                            dnsSettings: true,\n                                                            logSettings: false,\n                                                            zones: true,\n                                                            allowedZones: true,\n                                                            blockedZones: true,\n                                                            blockLists: true,\n                                                            apps: true,\n                                                            scopes: false,\n                                                            stats: false,\n                                                            logs: false,\n                                                            deleteExistingFiles: false,\n                                                            isConfigTransfer: true);\n\n                    _configLastSynced = response.Item2;\n\n                    //save config\n                    SaveConfigFile();\n                }\n\n                _dnsWebService.LogManager.Write(\"Server configuration was synced from the Primary node successfully.\");\n            }\n            finally\n            {\n                try\n                {\n                    File.Delete(tmpFile);\n                }\n                catch (Exception ex)\n                {\n                    _dnsWebService.LogManager.Write(ex);\n                }\n            }\n        }\n\n        private void TriggerClusterUpdateForSecondaryNodeChanges()\n        {\n            if (_clusterUpdateForSecondaryNodeChangesTimerTriggered)\n                return;\n\n            _clusterUpdateForSecondaryNodeChangesTimer.Change(CLUSTER_UPDATE_FOR_SECONDARY_NODE_CHANGES_TIMER_INTERVAL, Timeout.Infinite);\n            _clusterUpdateForSecondaryNodeChangesTimerTriggered = true;\n        }\n\n        private async void ClusterUpdateForSecondaryNodeChangesTimerCallbackAsync(object state)\n        {\n            try\n            {\n                ClusterNode primaryNode = GetPrimaryNode();\n\n                if (primaryNode.State == ClusterNodeState.Self)\n                    throw new InvalidOperationException();\n\n                ClusterNode secondaryNode = GetSelfNode();\n\n                if (secondaryNode.Type != ClusterNodeType.Secondary)\n                    throw new InvalidOperationException();\n\n                UpdateClusterFromPrimaryNode(await primaryNode.UpdateSecondaryNodeAsync(secondaryNode, _dnsWebService.WebServiceTlsCertificate));\n\n                _dnsWebService.LogManager.Write(\"DNS Server updated this Secondary node's details on the Primary node successfully.\");\n            }\n            catch (Exception ex)\n            {\n                _dnsWebService.LogManager.Write(\"DNS Server failed to update this Secondary node's details on the Primary node.\" + ex.ToString());\n            }\n            finally\n            {\n                _clusterUpdateForSecondaryNodeChangesTimerTriggered = false;\n            }\n        }\n\n        public void UpdateClusterFromPrimaryNode(ClusterInfo primaryNodeClusterInfo)\n        {\n            IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;\n\n            //validation\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n            {\n                if (existingClusterNode.Value.Type == ClusterNodeType.Primary)\n                {\n                    if (existingClusterNode.Value.State == ClusterNodeState.Self)\n                        throw new InvalidOperationException(); //this is a self primary node itself\n\n                    break;\n                }\n            }\n\n            List<ClusterNode> clusterNodesToAdd = new List<ClusterNode>();\n            List<ClusterNode> clusterNodesToRemove = new List<ClusterNode>();\n\n            foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in primaryNodeClusterInfo.ClusterNodes)\n            {\n                if (existingClusterNodes.TryGetValue(clusterNodeInfo.Id, out ClusterNode existingClusterNode))\n                {\n                    if (existingClusterNode.State == ClusterNodeState.Self)\n                        continue; //skip self node\n\n                    //update existing cluster node\n                    existingClusterNode.UpdateNode(clusterNodeInfo);\n                }\n                else\n                {\n                    //add new cluster node\n                    clusterNodesToAdd.Add(new ClusterNode(this, clusterNodeInfo));\n                }\n            }\n\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n            {\n                bool found = false;\n\n                foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in primaryNodeClusterInfo.ClusterNodes)\n                {\n                    if (existingClusterNode.Key == clusterNodeInfo.Id)\n                    {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found)\n                    clusterNodesToRemove.Add(existingClusterNode.Value);\n            }\n\n            bool saveConfig = false;\n\n            if ((clusterNodesToAdd.Count > 0) || (clusterNodesToRemove.Count > 0))\n            {\n                Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count + clusterNodesToAdd.Count - clusterNodesToRemove.Count);\n\n                foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n                {\n                    if (clusterNodesToRemove.Contains(existingClusterNode.Value))\n                        continue; //skip removed node\n\n                    updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;\n                }\n\n                foreach (ClusterNode clusterNode in clusterNodesToAdd)\n                    updatedClusterNodes[clusterNode.Id] = clusterNode;\n\n                //verify if node is part of the cluster\n                {\n                    bool foundSelfNode = false;\n\n                    foreach (KeyValuePair<int, ClusterNode> clusterNode in updatedClusterNodes)\n                    {\n                        if (clusterNode.Value.State == ClusterNodeState.Self)\n                        {\n                            foundSelfNode = true;\n                            break;\n                        }\n                    }\n\n                    if (!foundSelfNode)\n                    {\n                        //this node is not part of the cluster anymore\n                        //delete all cluster config\n                        DeleteAllClusterConfig();\n\n                        _dnsWebService.LogManager.Write(\"Failed to sync Cluster config: this Secondary node is not part of the Cluster anymore.\");\n                        return;\n                    }\n                }\n\n                //dispose all removed nodes\n                foreach (ClusterNode removedNodes in clusterNodesToRemove)\n                    removedNodes.Dispose();\n\n                _clusterNodes = updatedClusterNodes;\n\n                InitializeHeartbeatTimerFor(updatedClusterNodes);\n                saveConfig = true;\n            }\n\n            if (primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds != _heartbeatRefreshIntervalSeconds)\n            {\n                _heartbeatRefreshIntervalSeconds = primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds;\n                UpdateHeartbeatTimerForAllClusterNodes(); //apply new interval to all cluster nodes immediately\n                saveConfig = true;\n            }\n\n            if (primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds != _heartbeatRetryIntervalSeconds)\n            {\n                _heartbeatRetryIntervalSeconds = primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds;\n                saveConfig = true;\n            }\n\n            if (primaryNodeClusterInfo.ConfigRefreshIntervalSeconds != _configRefreshIntervalSeconds)\n            {\n                _configRefreshIntervalSeconds = primaryNodeClusterInfo.ConfigRefreshIntervalSeconds;\n                UpdateConfigRefreshTimer(_configRefreshIntervalSeconds * 1000); //apply new interval to config refresh timer immediately\n                saveConfig = true;\n            }\n\n            if (primaryNodeClusterInfo.ConfigRetryIntervalSeconds != _configRetryIntervalSeconds)\n            {\n                _configRetryIntervalSeconds = primaryNodeClusterInfo.ConfigRetryIntervalSeconds;\n                saveConfig = true;\n            }\n\n            //save changes\n            if (saveConfig)\n                SaveConfigFile();\n        }\n\n        public async Task PromoteToPrimaryNodeAsync(bool forceDeletePrimary)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to promote to Primary node: the Cluster is not initialized.\");\n\n            //do validation\n            ClusterNode selfNewPrimaryNode = GetSelfNode();\n            if (selfNewPrimaryNode.Type != ClusterNodeType.Secondary)\n                throw new DnsServerException(\"Failed to promote to Primary node: only Secondary nodes can be promoted to Primary nodes.\");\n\n            string clusterCatalogDomain = \"cluster-catalog.\" + _clusterDomain;\n\n            AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain);\n            if (clusterCatalogZoneInfo is null)\n                throw new DnsServerException(\"Failed to promote to Primary node: the Cluster Secondary Catalog zone does not exist.\");\n\n            AuthZoneInfo clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(_clusterDomain);\n            if (clusterZoneInfo is null)\n                throw new DnsServerException(\"Failed to promote to Primary node: the Cluster Secondary zone does not exist.\");\n\n            //stop cluster config refresh timer\n            StopConfigRefreshTimer();\n\n            //resync config and delete current primary node from the cluster immediately\n            ClusterNode existingPrimaryNode = GetPrimaryNode();\n\n            if (!forceDeletePrimary)\n            {\n                //resync complete config from current primary node to ensure all data is synced\n                _configLastSynced = DateTime.UnixEpoch; //to ensure complete config resync\n                await existingPrimaryNode.SyncConfigAsync();\n\n                //delete current cluster primary node\n                await existingPrimaryNode.DeleteClusterAsync(true);\n            }\n\n            //dispose primary node immediately to stop heartbeat\n            existingPrimaryNode.Dispose();\n\n            //remove primary node from cluster nodes\n            IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;\n            Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count - 1);\n\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n            {\n                if (existingClusterNode.Key == existingPrimaryNode.Id)\n                    continue;\n\n                updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value;\n            }\n\n            //update cluster nodes\n            _clusterNodes = updatedClusterNodes;\n\n            //promote self node to primary immediately\n            selfNewPrimaryNode.PromoteToPrimaryNode();\n\n            //convert cluster secondary catalog zone to catalog zone along with all its member zones\n            if (clusterCatalogZoneInfo.Type == AuthZoneType.SecondaryCatalog)\n                clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.ConvertZoneTypeTo(clusterCatalogZoneInfo.Name, AuthZoneType.Catalog);\n\n            //get converted primary cluster zone info\n            clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(_clusterDomain);\n            if (clusterZoneInfo is null)\n                throw new DnsServerException(\"Failed to promote to Primary node: the Cluster Primary zone does not exist.\");\n\n            //sign cluster zone in case when DNSSEC private keys were not available during ConvertZoneTypeTo() operation\n            if (clusterZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned)\n            {\n                DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.KeySigningKey);\n                DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.ZoneSigningKey);\n                zskPrivateKey.RolloverDays = 90;\n\n                _dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZone(clusterZoneInfo.Name, kskPrivateKey, zskPrivateKey, 3600, false);\n            }\n\n            //find existing record TTL values\n            FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);\n\n            //remove old primary node records from cluster primary zone and save zone file\n            if (existingPrimaryNode is not null)\n                RemoveClusterPrimaryZoneRecordsFor(existingPrimaryNode);\n\n            //update cluster primary zone for new primary node\n            AddClusterPrimaryZoneRecordsFor(selfNewPrimaryNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);\n\n            //update cluster catalog zone ACLs, TSIG key name and save zone file\n            UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo);\n\n            //save all changes\n            SaveConfigFile();\n\n            //notify all secondary nodes as a primary node immediately\n            TriggerNotifyAllSecondaryNodes(0);\n\n            //trigger NS and SOA update for member zones\n            TriggerRecordUpdateForClusterCatalogMemberZones();\n        }\n\n        #endregion\n\n        #region public\n\n        public ClusterNode GetPrimaryNode()\n        {\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n            if (clusterNodes is null)\n                throw new InvalidOperationException();\n\n            foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n            {\n                if (clusterNode.Value.Type == ClusterNodeType.Primary)\n                    return clusterNode.Value;\n            }\n\n            throw new InvalidOperationException();\n        }\n\n        public ClusterNode GetSelfNode()\n        {\n            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n            if (clusterNodes is null)\n                throw new InvalidOperationException();\n\n            foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n            {\n                if (clusterNode.Value.State == ClusterNodeState.Self)\n                    return clusterNode.Value;\n            }\n\n            throw new InvalidOperationException();\n        }\n\n        public bool TryGetClusterNode(string nodeName, out ClusterNode clusterNode)\n        {\n            foreach (KeyValuePair<int, ClusterNode> node in _clusterNodes)\n            {\n                if (node.Value.Name.Equals(nodeName, StringComparison.OrdinalIgnoreCase))\n                {\n                    clusterNode = node.Value;\n                    return true;\n                }\n            }\n\n            clusterNode = null;\n            return false;\n        }\n\n        public bool IsClusterPrimaryZone(string zoneName)\n        {\n            return (zoneName is not null) && zoneName.Equals(_clusterDomain, StringComparison.OrdinalIgnoreCase);\n        }\n\n        public bool IsClusterCatalogZone(string zoneName)\n        {\n            return (zoneName is not null) && zoneName.Equals(\"cluster-catalog.\" + _clusterDomain, StringComparison.OrdinalIgnoreCase);\n        }\n\n        public ClusterNode UpdateSelfNodeIPAddresses(IReadOnlyList<IPAddress> ipAddresses)\n        {\n            ClusterNode selfNode = GetSelfNode();\n\n            switch (selfNode.Type)\n            {\n                case ClusterNodeType.Primary:\n                    //find existing record TTL values\n                    FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);\n\n                    //update cluster zone to remove current self node records\n                    RemoveClusterPrimaryZoneRecordsFor(selfNode);\n\n                    //update self node\n                    selfNode.UpdateSelfNodeIPAddresses(ipAddresses);\n\n                    //update cluster zone to add updated self node records\n                    AddClusterPrimaryZoneRecordsFor(selfNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);\n\n                    //update cluster catalog zone ACLs and save zone file\n                    UpdateClusterCatalogZoneOptions();\n\n                    //save all changes\n                    SaveConfigFile();\n\n                    //notify all secondary nodes immediately\n                    TriggerNotifyAllSecondaryNodes(0);\n                    break;\n\n                case ClusterNodeType.Secondary:\n                    //update self node\n                    selfNode.UpdateSelfNodeIPAddresses(ipAddresses);\n\n                    //save all changes\n                    SaveConfigFile();\n\n                    //trigger cluster node update on primary node\n                    TriggerClusterUpdateForSecondaryNodeChanges();\n                    break;\n            }\n\n            return selfNode;\n        }\n\n        public void UpdateSelfNodeUrlAndCertificate()\n        {\n            ClusterNode selfNode = GetSelfNode();\n\n            //validation\n            foreach (KeyValuePair<int, ClusterNode> clusterNode in _clusterNodes)\n            {\n                if (clusterNode.Key == selfNode.Id)\n                    continue; //skip self\n\n                if (clusterNode.Value.Name.Equals(_dnsWebService.DnsServer.ServerDomain, StringComparison.OrdinalIgnoreCase))\n                    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.\");\n            }\n\n            switch (selfNode.Type)\n            {\n                case ClusterNodeType.Primary:\n                    //find existing record TTL values\n                    FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl);\n\n                    //update cluster zone to remove current self node records\n                    RemoveClusterPrimaryZoneRecordsFor(selfNode);\n\n                    //update self node\n                    selfNode.UpdateSelfNodeUrl();\n\n                    //update cluster zone to add updated self node records\n                    AddClusterPrimaryZoneRecordsFor(selfNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate);\n\n                    //save all changes\n                    SaveConfigFile();\n\n                    _dnsWebService.LogManager.Write(\"Primary node '\" + selfNode.ToString() + \"' URL was updated successfully.\");\n\n                    //notify all secondary nodes\n                    TriggerNotifyAllSecondaryNodes();\n                    break;\n\n                case ClusterNodeType.Secondary:\n                    //update self node\n                    selfNode.UpdateSelfNodeUrl();\n\n                    //save all changes\n                    SaveConfigFile();\n\n                    _dnsWebService.LogManager.Write(\"Secondary node '\" + selfNode.ToString() + \"' URL was updated successfully.\");\n\n                    //trigger cluster node update on primary node\n                    TriggerClusterUpdateForSecondaryNodeChanges();\n                    break;\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public DnsWebService DnsWebService\n        { get { return _dnsWebService; } }\n\n        public bool ClusterInitialized\n        {\n            get\n            {\n                IReadOnlyDictionary<int, ClusterNode> clusterNodes = _clusterNodes;\n\n                return (clusterNodes is not null) && (clusterNodes.Count > 0);\n            }\n        }\n\n        public string ClusterDomain\n        { get { return _clusterDomain; } }\n\n        public ushort HeartbeatRefreshIntervalSeconds\n        { get { return _heartbeatRefreshIntervalSeconds; } }\n\n        public ushort HeartBeatRetryIntervalSeconds\n        { get { return _heartbeatRetryIntervalSeconds; } }\n\n        public ushort ConfigRefreshIntervalSeconds\n        { get { return _configRefreshIntervalSeconds; } }\n\n        public ushort ConfigRetryIntervalSeconds\n        { get { return _configRetryIntervalSeconds; } }\n\n        public DateTime ConfigLastSynced\n        { get { return _configLastSynced; } }\n\n        public IReadOnlyDictionary<int, ClusterNode> ClusterNodes\n        { get { return _clusterNodes; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Cluster/ClusterNode.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.HttpApi;\nusing DnsServerCore.HttpApi.Models;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerCore.Cluster\n{\n    enum ClusterNodeType : byte\n    {\n        Unknown = 0,\n        Primary = 1,\n        Secondary = 2\n    }\n\n    enum ClusterNodeState : byte\n    {\n        Unknown = 0,\n        Self = 1,\n        Connected = 2,\n        Unreachable = 3\n    }\n\n    class ClusterNode : IComparable<ClusterNode>, IDisposable\n    {\n        #region variables\n\n        readonly ClusterManager _clusterManager;\n\n        readonly int _id;\n        Uri _url;\n        IReadOnlyList<IPAddress> _ipAddresses;\n        ClusterNodeType _type;\n        ClusterNodeState _state;\n\n        DateTime _upSince;\n        DateTime _lastSeen;\n        HttpApiClient _apiClient;\n\n        Timer _heartbeatTimer;\n        const int HEARTBEAT_TIMER_INITIAL_INTERVAL = 5000;\n\n        #endregion\n\n        #region constructor\n\n        public ClusterNode(ClusterManager clusterManager, ClusterInfo.ClusterNodeInfo nodeInfo)\n        {\n            _clusterManager = clusterManager;\n\n            _id = nodeInfo.Id;\n            _url = nodeInfo.Url;\n            _ipAddresses = nodeInfo.IPAddresses.Convert(IPAddress.Parse);\n            _type = Enum.Parse<ClusterNodeType>(nodeInfo.Type, true);\n\n            if (_type == ClusterNodeType.Primary)\n            {\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected; //since this info was received from primary node\n            }\n            else\n            {\n                _state = ClusterNodeState.Unknown;\n            }\n        }\n\n        public ClusterNode(ClusterManager clusterManager, int id, Uri url, IReadOnlyList<IPAddress> ipAddresses, ClusterNodeType type, ClusterNodeState state)\n        {\n            if (url.OriginalString.Length > 255)\n                throw new ArgumentException(\"Cluster node URL length must be less than 255 bytes.\", nameof(url));\n\n            if (!url.Scheme.Equals(\"https\", StringComparison.OrdinalIgnoreCase))\n                throw new ArgumentException(\"Cluster node URL must use HTTPS scheme.\", nameof(url));\n\n            if (ipAddresses.Count > 10)\n                throw new ArgumentException(\"Cluster node cannot have more than 10 IP addresses.\", nameof(ipAddresses));\n\n            _clusterManager = clusterManager;\n\n            _id = id;\n            _url = url;\n            _ipAddresses = ipAddresses;\n            _type = type;\n            _state = state;\n        }\n\n        public ClusterNode(ClusterManager clusterManager, BinaryReader bR)\n        {\n            _clusterManager = clusterManager;\n\n            int version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                    _id = bR.ReadInt32();\n                    _url = new Uri(bR.ReadShortString());\n\n                    if (version >= 2)\n                    {\n                        int count = bR.ReadByte();\n                        IPAddress[] ipAddresses = new IPAddress[count];\n\n                        for (int i = 0; i < count; i++)\n                            ipAddresses[i] = IPAddressExtensions.ReadFrom(bR);\n\n                        _ipAddresses = ipAddresses;\n                    }\n                    else\n                    {\n                        _ipAddresses = [IPAddressExtensions.ReadFrom(bR)];\n                    }\n\n                    _type = (ClusterNodeType)bR.ReadByte();\n                    _state = (ClusterNodeState)bR.ReadByte();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"Cluster Node version not supported.\");\n            }\n\n            if (_state != ClusterNodeState.Self)\n                _state = ClusterNodeState.Unknown;\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _heartbeatTimer?.Dispose();\n\n            if (_apiClient is not null)\n            {\n                ThreadPool.QueueUserWorkItem(async delegate (object state)\n                {\n                    try\n                    {\n                        await Task.Delay(2000); //give some time for any in-progress API calls to complete\n                        _apiClient?.Dispose();\n                    }\n                    catch\n                    { }\n                });\n            }\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private HttpApiClient GetApiClient()\n        {\n            if (_state == ClusterNodeState.Self)\n                throw new InvalidOperationException();\n\n            if (_apiClient is null)\n            {\n                _apiClient = new HttpApiClient(_url, _clusterManager.DnsWebService.DnsServer.Proxy, _clusterManager.DnsWebService.DnsServer.PreferIPv6, false, new InternalDnsClient(_clusterManager.DnsWebService.DnsServer, this));\n\n                UserSession clusterApiToken = null;\n\n                foreach (UserSession session in _clusterManager.DnsWebService.AuthManager.Sessions)\n                {\n                    if ((session.Type == UserSessionType.ApiToken) && (session.TokenName == _clusterManager.ClusterDomain))\n                    {\n                        clusterApiToken = session;\n                        break;\n                    }\n                }\n\n                if (clusterApiToken is null)\n                    throw new InvalidOperationException(\"No API token was found for the Cluster domain.\");\n\n                _apiClient.UseApiToken(clusterApiToken.Token);\n            }\n\n            return _apiClient;\n        }\n\n        private async void HeartbeatTimerCallbackAsync(object state)\n        {\n            bool success = true;\n\n            try\n            {\n                ClusterInfo clusterInfo = await GetClusterStateAsync();\n\n                if (_type == ClusterNodeType.Primary)\n                    _clusterManager.UpdateClusterFromPrimaryNode(clusterInfo); //update cluster nodes from primary node response\n\n                //update up since time\n                foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in clusterInfo.ClusterNodes)\n                {\n                    if (clusterNodeInfo.Name.Equals(Name, StringComparison.OrdinalIgnoreCase))\n                    {\n                        _upSince = clusterNodeInfo.UpSince ?? default;\n                        break;\n                    }\n                }\n            }\n            catch (TaskCanceledException)\n            {\n                //ignore\n            }\n            catch (Exception ex)\n            {\n                success = false;\n                _clusterManager.DnsWebService.LogManager.Write(\"Heartbeat failed for \" + _type.ToString() + \" node '\" + ToString() + \"'.\\r\\n\" + ex.ToString());\n            }\n            finally\n            {\n                try\n                {\n                    _heartbeatTimer?.Change(success ? _clusterManager.HeartbeatRefreshIntervalSeconds * 1000 : _clusterManager.HeartBeatRetryIntervalSeconds * 1000, Timeout.Infinite);\n                }\n                catch (ObjectDisposedException)\n                { }\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void PromoteToPrimaryNode()\n        {\n            _type = ClusterNodeType.Primary;\n        }\n\n        public void UpdateSelfNodeIPAddresses(IReadOnlyList<IPAddress> ipAddresses)\n        {\n            if (_state != ClusterNodeState.Self)\n                throw new InvalidOperationException();\n\n            if (ipAddresses.Count > 10)\n                throw new ArgumentException(\"Cluster node cannot have more than 10 IP addresses.\", nameof(ipAddresses));\n\n            _ipAddresses = ipAddresses;\n        }\n\n        public void UpdateSelfNodeUrl()\n        {\n            if (_state != ClusterNodeState.Self)\n                throw new InvalidOperationException();\n\n            Uri url = new Uri($\"https://{_clusterManager.DnsWebService.DnsServer.ServerDomain}:{_clusterManager.DnsWebService.WebServiceTlsPort}/\");\n\n            if (url.OriginalString.Length > 255)\n                throw new ArgumentException(\"Cluster node URL length must be less than 255 bytes.\", nameof(url));\n\n            if (!url.Scheme.Equals(\"https\", StringComparison.OrdinalIgnoreCase))\n                throw new ArgumentException(\"Cluster node URL must use HTTPS scheme.\", nameof(url));\n\n            _url = url;\n        }\n\n        public void UpdateNode(Uri url, IReadOnlyList<IPAddress> ipAddresses)\n        {\n            if (url.OriginalString.Length > 255)\n                throw new ArgumentException(\"Cluster node URL length must be less than 255 bytes.\", nameof(url));\n\n            if (!url.Scheme.Equals(\"https\", StringComparison.OrdinalIgnoreCase))\n                throw new ArgumentException(\"Cluster node URL must use HTTPS scheme.\", nameof(url));\n\n            if (ipAddresses.Count > 10)\n                throw new ArgumentException(\"Cluster node cannot have more than 10 IP addresses.\", nameof(ipAddresses));\n\n            bool changed = false;\n\n            if (!_url.Equals(url))\n            {\n                _url = url;\n                changed = true;\n            }\n\n            if (!_ipAddresses.HasSameItems(ipAddresses))\n            {\n                _ipAddresses = ipAddresses;\n                changed = true;\n            }\n\n            if (changed && (_apiClient is not null))\n            {\n                _apiClient.Dispose();\n                _apiClient = null;\n            }\n        }\n\n        public void UpdateNode(ClusterInfo.ClusterNodeInfo nodeInfo)\n        {\n            if (nodeInfo.Id != _id)\n                throw new InvalidOperationException();\n\n            bool changed = false;\n\n            if (!_url.Equals(nodeInfo.Url))\n            {\n                _url = nodeInfo.Url;\n                changed = true;\n            }\n\n            IReadOnlyList<IPAddress> ipAddresses = nodeInfo.IPAddresses.Convert(IPAddress.Parse);\n            if (!_ipAddresses.HasSameItems(ipAddresses))\n            {\n                _ipAddresses = ipAddresses;\n                changed = true;\n            }\n\n            _type = Enum.Parse<ClusterNodeType>(nodeInfo.Type, true);\n\n            if (changed && (_apiClient is not null))\n            {\n                _apiClient.Dispose();\n                _apiClient = null;\n            }\n        }\n\n        public void InitializeHeartbeatTimer()\n        {\n            if (_state == ClusterNodeState.Self)\n                throw new InvalidOperationException();\n\n            if (_heartbeatTimer is null)\n            {\n                _heartbeatTimer = new Timer(HeartbeatTimerCallbackAsync);\n\n                //for Primary node use configured refresh interval since config transfer already syncs the cluster state\n                _heartbeatTimer.Change(_type == ClusterNodeType.Primary ? _clusterManager.HeartbeatRefreshIntervalSeconds * 1000 : HEARTBEAT_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        public void UpdateHeartbeatTimer()\n        {\n            if (_state == ClusterNodeState.Self)\n                throw new InvalidOperationException();\n\n            _heartbeatTimer?.Change(_clusterManager.HeartbeatRefreshIntervalSeconds * 1000, Timeout.Infinite);\n        }\n\n        public async Task<DashboardStats> 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)\n        {\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                DashboardStats stats = await apiClient.GetDashboardStatsAsync(sessionUser.Username, type, utcFormat, acceptLanguage, dontTrimQueryTypeData, startDate, endDate, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n\n                return stats;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task<DashboardStats> GetDashboardTopStatsAsync(User sessionUser, DashboardTopStatsType statsType, int limit = 1000, DashboardStatsType type = DashboardStatsType.LastHour, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default)\n        {\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                DashboardStats stats = await apiClient.GetDashboardTopStatsAsync(sessionUser.Username, statsType, limit, type, startDate, endDate, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n\n                return stats;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task SetClusterSettingsAsync(User sessionUser, IReadOnlyDictionary<string, string> clusterParameters, CancellationToken cancellationToken = default)\n        {\n            if (_type != ClusterNodeType.Primary)\n                throw new InvalidOperationException();\n\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                await apiClient.SetClusterSettingsAsync(sessionUser.Username, clusterParameters, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task ForceUpdateBlockListsAsync(User sessionUser, CancellationToken cancellationToken = default)\n        {\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                await apiClient.ForceUpdateBlockListsAsync(sessionUser.Username, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task TemporaryDisableBlockingAsync(User sessionUser, int minutes, CancellationToken cancellationToken = default)\n        {\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                await apiClient.TemporaryDisableBlockingAsync(sessionUser.Username, minutes, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task<ClusterInfo> GetClusterStateAsync(CancellationToken cancellationToken = default)\n        {\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                ClusterInfo clusterInfo = await apiClient.GetClusterStateAsync(false, true, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n\n                return clusterInfo;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task<ClusterInfo> DeleteClusterAsync(bool forceDelete = false, CancellationToken cancellationToken = default)\n        {\n            if (_type != ClusterNodeType.Primary)\n                throw new InvalidOperationException();\n\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                ClusterInfo clusterInfo = await apiClient.DeleteClusterAsync(forceDelete, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Unreachable; //node is deleted, so mark as unreachable\n\n                return clusterInfo;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task NotifySecondaryNodeAsync(ClusterNode primaryNode, CancellationToken cancellationToken = default)\n        {\n            if (_type != ClusterNodeType.Secondary)\n                throw new InvalidOperationException();\n\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                await apiClient.NotifySecondaryNodeAsync(primaryNode.Id, primaryNode._url, primaryNode._ipAddresses, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n\n                _clusterManager.DnsWebService.LogManager.Write(\"DNS Server successfully notified Secondary node '\" + ToString() + \"' for server configuration changes.\");\n            }\n            catch (Exception ex)\n            {\n                _state = ClusterNodeState.Unreachable;\n\n                _clusterManager.DnsWebService.LogManager.Write(\"DNS Server failed to notify Secondary node '\" + ToString() + \"' for server configuration changes.\\r\\n\" + ex.ToString());\n            }\n        }\n\n        public async Task SyncConfigAsync(IReadOnlyCollection<string> includeZones = null, CancellationToken cancellationToken = default)\n        {\n            if (_type != ClusterNodeType.Primary)\n                throw new InvalidOperationException();\n\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                await _clusterManager.SyncConfigFromAsync(apiClient, includeZones, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task AskSecondaryNodeToLeaveClusterAsync(CancellationToken cancellationToken = default)\n        {\n            if (_type != ClusterNodeType.Secondary)\n                throw new InvalidOperationException();\n\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                _ = await apiClient.LeaveClusterAsync(cancellationToken: cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task DeleteSecondaryNodeAsync(ClusterNode secondaryNode, CancellationToken cancellationToken = default)\n        {\n            if (_type != ClusterNodeType.Primary)\n                throw new InvalidOperationException();\n\n            if (secondaryNode.Type != ClusterNodeType.Secondary)\n                throw new InvalidOperationException();\n\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                await apiClient.DeleteSecondaryNodeAsync(secondaryNode._id, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task<ClusterInfo> UpdateSecondaryNodeAsync(ClusterNode secondaryNode, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default)\n        {\n            if (_type != ClusterNodeType.Primary)\n                throw new InvalidOperationException();\n\n            if (secondaryNode.Type != ClusterNodeType.Secondary)\n                throw new InvalidOperationException();\n\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                ClusterInfo clusterInfo = await apiClient.UpdateSecondaryNodeAsync(secondaryNode._id, secondaryNode._url, secondaryNode._ipAddresses, secondaryNodeCertificate, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n\n                return clusterInfo;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public async Task ProxyRequest(HttpContext context, string actingUsername, CancellationToken cancellationToken = default)\n        {\n            HttpApiClient apiClient = GetApiClient();\n\n            try\n            {\n                await apiClient.ProxyRequest(context, actingUsername, cancellationToken);\n\n                _lastSeen = DateTime.UtcNow;\n                _state = ClusterNodeState.Connected;\n            }\n            catch\n            {\n                _state = ClusterNodeState.Unreachable;\n                throw;\n            }\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)2); //version\n            bW.Write(_id);\n            bW.WriteShortString(_url.OriginalString);\n\n            bW.Write(Convert.ToByte(_ipAddresses.Count));\n\n            foreach (IPAddress ipAddress in _ipAddresses)\n                ipAddress.WriteTo(bW);\n\n            bW.Write((byte)_type);\n            bW.Write((byte)_state);\n        }\n\n        public override string ToString()\n        {\n            return _url.Host.ToLowerInvariant() + \" (\" + _ipAddresses.Join() + \")\";\n        }\n\n        public int CompareTo(ClusterNode other)\n        {\n            return _url.Host.CompareTo(other._url.Host);\n        }\n\n        #endregion\n\n        #region properties\n\n        public int Id\n        { get { return _id; } }\n\n        public string Name\n        { get { return _url.Host.ToLowerInvariant(); } }\n\n        public Uri Url\n        { get { return _url; } }\n\n        public IReadOnlyList<IPAddress> IPAddresses\n        { get { return _ipAddresses; } }\n\n        public ClusterNodeType Type\n        { get { return _type; } }\n\n        public ClusterNodeState State\n        { get { return _state; } }\n\n        public DateTime UpSince\n        {\n            get\n            {\n                if (_state == ClusterNodeState.Self)\n                    return _clusterManager.DnsWebService.UpTimeStamp;\n\n                return _upSince;\n            }\n        }\n\n        public DateTime LastSeen\n        { get { return _lastSeen; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Cluster/InternalDnsClient.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Cluster\n{\n    class InternalDnsClient : IDnsClient\n    {\n        #region variables\n\n        readonly DnsServer _dnsServer;\n        readonly ClusterNode _clusterNode;\n        readonly IReadOnlyList<IPAddress> _ipAddresses;\n\n        #endregion\n\n        #region constructor\n\n        public InternalDnsClient(DnsServer dnsServer, ClusterNode clusterNode)\n        {\n            _dnsServer = dnsServer;\n            _clusterNode = clusterNode;\n        }\n\n        public InternalDnsClient(DnsServer dnsServer, IReadOnlyList<IPAddress> ipAddresses)\n        {\n            _dnsServer = dnsServer;\n            _ipAddresses = ipAddresses;\n        }\n\n        #endregion\n\n        #region protected\n\n        public Task<DnsDatagram> ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default)\n        {\n            switch (question.Type)\n            {\n                case DnsResourceRecordType.A:\n                case DnsResourceRecordType.AAAA:\n                    IReadOnlyList<IPAddress> ipAddresses;\n\n                    if (_clusterNode is null)\n                        ipAddresses = _ipAddresses;\n                    else\n                        ipAddresses = _clusterNode.IPAddresses;\n\n                    List<DnsResourceRecord> answer = new List<DnsResourceRecord>();\n\n                    foreach (IPAddress ipAddress in ipAddresses)\n                    {\n                        DnsResourceRecordData rdata = null;\n\n                        switch (ipAddress.AddressFamily)\n                        {\n                            case AddressFamily.InterNetwork:\n                                if (question.Type == DnsResourceRecordType.A)\n                                    rdata = new DnsARecordData(ipAddress);\n\n                                break;\n\n                            case AddressFamily.InterNetworkV6:\n                                if (question.Type == DnsResourceRecordType.AAAA)\n                                    rdata = new DnsAAAARecordData(ipAddress);\n\n                                break;\n                        }\n\n                        if (rdata is not null)\n                            answer.Add(new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, 30, rdata));\n                    }\n\n                    return Task.FromResult(new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, false, DnsResponseCode.NoError, [question], answer));\n\n                default:\n                    DirectDnsClient dnsClient = new DirectDnsClient(_dnsServer);\n                    dnsClient.DnssecValidation = true;\n\n                    //load latest trust anchors into dns client\n                    _dnsServer.AuthZoneManager.LoadTrustAnchorsTo(dnsClient, question.Name, question.Type);\n\n                    return dnsClient.ResolveAsync(question, cancellationToken);\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/DhcpMessage.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dhcp.Options;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp\n{\n    enum DhcpMessageOpCode : byte\n    {\n        BootRequest = 1,\n        BootReply = 2\n    }\n\n    enum DhcpMessageHardwareAddressType : byte\n    {\n        Ethernet = 1\n    }\n\n    enum DhcpMessageFlags : ushort\n    {\n        None = 0,\n        Broadcast = 0x8000\n    }\n\n    class DhcpMessage\n    {\n        #region variables\n\n        const uint MAGIC_COOKIE = 0x63538263; //in reverse format\n\n        readonly DhcpMessageOpCode _op;\n        readonly DhcpMessageHardwareAddressType _htype;\n        readonly byte _hlen;\n        readonly byte _hops;\n\n        readonly byte[] _xid;\n\n        readonly byte[] _secs;\n        readonly DhcpMessageFlags _flags;\n\n        readonly IPAddress _ciaddr;\n        readonly IPAddress _yiaddr;\n        readonly IPAddress _siaddr;\n        readonly IPAddress _giaddr;\n\n        readonly byte[] _chaddr;\n        readonly byte[] _sname;\n        readonly byte[] _file;\n\n        readonly IReadOnlyCollection<DhcpOption> _options;\n\n        readonly byte[] _clientHardwareAddress;\n        readonly string _serverHostName;\n        readonly string _bootFileName;\n\n        OptionOverloadOption _optionOverload;\n\n        DhcpMessageTypeOption _dhcpMessageType;\n        VendorClassIdentifierOption _vendorClassIdentifier;\n        ClientIdentifierOption _clientIdentifier;\n        ClientIdentifierOption _clientHardwareIdentifier;\n        HostNameOption _hostName;\n        ClientFullyQualifiedDomainNameOption _clientFullyQualifiedDomainName;\n        ParameterRequestListOption _parameterRequestList;\n        MaximumDhcpMessageSizeOption _maximumDhcpMessageSize;\n        ServerIdentifierOption _serverIdentifier;\n        RequestedIpAddressOption _requestedIpAddress;\n\n        #endregion\n\n        #region constructor\n\n        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<DhcpOption> options)\n        {\n            if (ciaddr.AddressFamily != AddressFamily.InterNetwork)\n                throw new ArgumentException(\"Address family not supported.\", nameof(ciaddr));\n\n            if (yiaddr.AddressFamily != AddressFamily.InterNetwork)\n                throw new ArgumentException(\"Address family not supported.\", nameof(yiaddr));\n\n            if (siaddr.AddressFamily != AddressFamily.InterNetwork)\n                throw new ArgumentException(\"Address family not supported.\", nameof(siaddr));\n\n            if (giaddr.AddressFamily != AddressFamily.InterNetwork)\n                throw new ArgumentException(\"Address family not supported.\", nameof(giaddr));\n\n            ArgumentNullException.ThrowIfNull(clientHardwareAddress);\n\n            if (clientHardwareAddress.Length > 16)\n                throw new ArgumentException(\"Client hardware address cannot exceed 16 bytes.\", nameof(clientHardwareAddress));\n\n            if (xid.Length != 4)\n                throw new ArgumentException(\"Transaction ID must be 4 bytes.\", nameof(xid));\n\n            if (secs.Length != 2)\n                throw new ArgumentException(\"Seconds elapsed must be 2 bytes.\", nameof(secs));\n\n            _op = op;\n            _htype = hardwareAddressType;\n            _hlen = Convert.ToByte(clientHardwareAddress.Length);\n            _hops = 0;\n\n            _xid = xid;\n\n            _secs = secs;\n            _flags = flags;\n\n            _ciaddr = ciaddr;\n            _yiaddr = yiaddr;\n            _siaddr = siaddr;\n            _giaddr = giaddr;\n\n            _clientHardwareAddress = clientHardwareAddress;\n            _chaddr = new byte[16];\n            Buffer.BlockCopy(_clientHardwareAddress, 0, _chaddr, 0, _clientHardwareAddress.Length);\n\n            _sname = new byte[64];\n            if (sname != null)\n            {\n                _serverHostName = sname;\n\n                byte[] buffer = Encoding.ASCII.GetBytes(sname);\n                if (buffer.Length >= 64)\n                    throw new ArgumentException(\"Server host name cannot exceed 63 bytes.\", nameof(sname));\n\n                Buffer.BlockCopy(buffer, 0, _sname, 0, buffer.Length);\n            }\n\n            _file = new byte[128];\n            if (file != null)\n            {\n                _bootFileName = file;\n\n                byte[] buffer = Encoding.ASCII.GetBytes(file);\n                if (buffer.Length >= 128)\n                    throw new ArgumentException(\"Boot file name cannot exceed 127 bytes.\", nameof(file));\n\n                Buffer.BlockCopy(buffer, 0, _file, 0, buffer.Length);\n            }\n\n            _options = options;\n\n            foreach (DhcpOption option in _options)\n            {\n                if (option.Code == DhcpOptionCode.ServerIdentifier)\n                {\n                    _serverIdentifier = option as ServerIdentifierOption;\n                    break;\n                }\n            }\n        }\n\n        public DhcpMessage(Stream s)\n        {\n            Span<byte> buffer = stackalloc byte[4];\n\n            s.ReadExactly(buffer);\n            _op = (DhcpMessageOpCode)buffer[0];\n            _htype = (DhcpMessageHardwareAddressType)buffer[1];\n            _hlen = buffer[2];\n            _hops = buffer[3];\n\n            _xid = s.ReadExactly(4);\n\n            s.ReadExactly(buffer);\n            _secs = new byte[2];\n            buffer.Slice(0, 2).CopyTo(_secs);\n            buffer.Reverse();\n            _flags = (DhcpMessageFlags)BitConverter.ToUInt16(buffer);\n\n            s.ReadExactly(buffer);\n            _ciaddr = new IPAddress(buffer);\n\n            s.ReadExactly(buffer);\n            _yiaddr = new IPAddress(buffer);\n\n            s.ReadExactly(buffer);\n            _siaddr = new IPAddress(buffer);\n\n            s.ReadExactly(buffer);\n            _giaddr = new IPAddress(buffer);\n\n            _chaddr = s.ReadExactly(16);\n            _clientHardwareAddress = new byte[_hlen];\n            Buffer.BlockCopy(_chaddr, 0, _clientHardwareAddress, 0, _hlen);\n\n            _sname = s.ReadExactly(64);\n            _file = s.ReadExactly(128);\n\n            //read options\n            List<DhcpOption> options = new List<DhcpOption>();\n            _options = options;\n\n            s.ReadExactly(buffer);\n            uint magicCookie = BitConverter.ToUInt32(buffer);\n\n            if (magicCookie == MAGIC_COOKIE)\n            {\n                ParseOptions(s, options);\n\n                if ((_optionOverload != null) && _optionOverload.Value.HasFlag(OptionOverloadValue.FileFieldUsed))\n                {\n                    using (MemoryStream mS = new MemoryStream(_file))\n                    {\n                        ParseOptions(mS, options);\n                    }\n                }\n                else\n                {\n                    for (int i = 0; i < _file.Length; i++)\n                    {\n                        if (_file[i] == 0)\n                        {\n                            if (i == 0)\n                                break;\n\n                            _bootFileName = Encoding.ASCII.GetString(_file, 0, i);\n                            break;\n                        }\n                    }\n                }\n\n                if ((_optionOverload != null) && _optionOverload.Value.HasFlag(OptionOverloadValue.SnameFieldUsed))\n                {\n                    using (MemoryStream mS = new MemoryStream(_sname))\n                    {\n                        ParseOptions(mS, options);\n                    }\n                }\n                else\n                {\n                    for (int i = 0; i < _sname.Length; i++)\n                    {\n                        if (_sname[i] == 0)\n                        {\n                            if (i == 0)\n                                break;\n\n                            _serverHostName = Encoding.ASCII.GetString(_sname, 0, i);\n                            break;\n                        }\n                    }\n                }\n\n                //parse all option values\n                foreach (DhcpOption option in options)\n                    option.ParseOptionValue();\n            }\n\n            if (_maximumDhcpMessageSize != null)\n                _maximumDhcpMessageSize = new MaximumDhcpMessageSizeOption(576);\n        }\n\n        #endregion\n\n        #region static\n\n        public static DhcpMessage CreateReply(DhcpMessage request, IPAddress yiaddr, IPAddress siaddr, string sname, string file, IReadOnlyCollection<DhcpOption> options)\n        {\n            return new DhcpMessage(DhcpMessageOpCode.BootReply, request.HardwareAddressType, request.TransactionId, request.SecondsElapsed, request.Flags, request.ClientIpAddress, yiaddr, siaddr, request.RelayAgentIpAddress, request.ClientHardwareAddress, sname, file, options);\n        }\n\n        #endregion\n\n        #region private\n\n        private void ParseOptions(Stream s, List<DhcpOption> options)\n        {\n            while (true)\n            {\n                DhcpOption option = DhcpOption.Parse(s);\n                if (option.Code == DhcpOptionCode.End)\n                    break;\n\n                if (option.Code == DhcpOptionCode.Pad)\n                    continue;\n\n                bool optionExists = false;\n\n                foreach (DhcpOption existingOption in options)\n                {\n                    if (existingOption.Code == option.Code)\n                    {\n                        //option already exists so append current option value into existing option\n                        existingOption.AppendOptionValue(option);\n                        optionExists = true;\n                        break;\n                    }\n                }\n\n                if (optionExists)\n                    continue;\n\n                //add option to list\n                options.Add(option);\n\n                switch (option.Code)\n                {\n                    case DhcpOptionCode.DhcpMessageType:\n                        _dhcpMessageType = option as DhcpMessageTypeOption;\n                        break;\n\n                    case DhcpOptionCode.VendorClassIdentifier:\n                        _vendorClassIdentifier = option as VendorClassIdentifierOption;\n                        break;\n\n                    case DhcpOptionCode.ClientIdentifier:\n                        _clientIdentifier = option as ClientIdentifierOption;\n                        break;\n\n                    case DhcpOptionCode.HostName:\n                        _hostName = option as HostNameOption;\n                        break;\n\n                    case DhcpOptionCode.ClientFullyQualifiedDomainName:\n                        _clientFullyQualifiedDomainName = option as ClientFullyQualifiedDomainNameOption;\n                        break;\n\n                    case DhcpOptionCode.ParameterRequestList:\n                        _parameterRequestList = option as ParameterRequestListOption;\n                        break;\n\n                    case DhcpOptionCode.MaximumDhcpMessageSize:\n                        _maximumDhcpMessageSize = option as MaximumDhcpMessageSizeOption;\n                        break;\n\n                    case DhcpOptionCode.ServerIdentifier:\n                        _serverIdentifier = option as ServerIdentifierOption;\n                        break;\n\n                    case DhcpOptionCode.RequestedIpAddress:\n                        _requestedIpAddress = option as RequestedIpAddressOption;\n                        break;\n\n                    case DhcpOptionCode.OptionOverload:\n                        _optionOverload = option as OptionOverloadOption;\n                        break;\n                }\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void WriteTo(Stream s)\n        {\n            s.WriteByte((byte)_op);\n            s.WriteByte((byte)_htype);\n            s.WriteByte(_hlen);\n            s.WriteByte(_hops);\n\n            s.Write(_xid);\n\n            s.Write(_secs);\n            byte[] buffer = BitConverter.GetBytes((ushort)_flags);\n            Array.Reverse(buffer);\n            s.Write(buffer);\n\n            s.Write(_ciaddr.GetAddressBytes());\n            s.Write(_yiaddr.GetAddressBytes());\n            s.Write(_siaddr.GetAddressBytes());\n            s.Write(_giaddr.GetAddressBytes());\n\n            s.Write(_chaddr);\n            s.Write(_sname);\n            s.Write(_file);\n\n            //write options\n            s.Write(BitConverter.GetBytes(MAGIC_COOKIE));\n\n            foreach (DhcpOption option in _options)\n                option.WriteTo(s);\n        }\n\n        public ClientIdentifierOption GetClientIdentifier(bool ignoreClientIdentifierOption)\n        {\n            if (ignoreClientIdentifierOption || (_clientIdentifier is null))\n            {\n                if (_clientHardwareIdentifier is null)\n                    _clientHardwareIdentifier = new ClientIdentifierOption((byte)_htype, _clientHardwareAddress);\n\n                return _clientHardwareIdentifier;\n            }\n\n            return _clientIdentifier;\n        }\n\n        public string GetClientFullIdentifier()\n        {\n            string hardwareAddress = BitConverter.ToString(_clientHardwareAddress);\n\n            if (_clientFullyQualifiedDomainName != null)\n                return _clientFullyQualifiedDomainName.DomainName + \" [\" + hardwareAddress + \"]\";\n\n            if (_hostName != null)\n                return _hostName.HostName + \" [\" + hardwareAddress + \"]\";\n\n            return \"[\" + hardwareAddress + \"]\";\n        }\n\n        #endregion\n\n        #region properties\n\n        public DhcpMessageOpCode OpCode\n        { get { return _op; } }\n\n        public DhcpMessageHardwareAddressType HardwareAddressType\n        { get { return _htype; } }\n\n        public byte HardwareAddressLength\n        { get { return _hlen; } }\n\n        public byte Hops\n        { get { return _hops; } }\n\n        public byte[] TransactionId\n        { get { return _xid; } }\n\n        public byte[] SecondsElapsed\n        { get { return _secs; } }\n\n        public DhcpMessageFlags Flags\n        { get { return _flags; } }\n\n        public IPAddress ClientIpAddress\n        { get { return _ciaddr; } }\n\n        public IPAddress YourClientIpAddress\n        { get { return _yiaddr; } }\n\n        public IPAddress NextServerIpAddress\n        { get { return _siaddr; } }\n\n        public IPAddress RelayAgentIpAddress\n        { get { return _giaddr; } }\n\n        public byte[] ClientHardwareAddress\n        { get { return _clientHardwareAddress; } }\n\n        public string ServerHostName\n        { get { return _serverHostName; } }\n\n        public string BootFileName\n        { get { return _bootFileName; } }\n\n        public IReadOnlyCollection<DhcpOption> Options\n        { get { return _options; } }\n\n        public DhcpMessageTypeOption DhcpMessageType\n        { get { return _dhcpMessageType; } }\n\n        public VendorClassIdentifierOption VendorClassIdentifier\n        { get { return _vendorClassIdentifier; } }\n\n        public HostNameOption HostName\n        { get { return _hostName; } }\n\n        public ClientFullyQualifiedDomainNameOption ClientFullyQualifiedDomainName\n        { get { return _clientFullyQualifiedDomainName; } }\n\n        public ParameterRequestListOption ParameterRequestList\n        { get { return _parameterRequestList; } }\n\n        public MaximumDhcpMessageSizeOption MaximumDhcpMessageSize\n        { get { return _maximumDhcpMessageSize; } }\n\n        public ServerIdentifierOption ServerIdentifier\n        { get { return _serverIdentifier; } }\n\n        public RequestedIpAddressOption RequestedIpAddress\n        { get { return _requestedIpAddress; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/DhcpOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dhcp.Options;\nusing System;\nusing System.IO;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp\n{\n    public enum DhcpOptionCode : byte\n    {\n        Pad = 0,\n        SubnetMask = 1,\n        TimeOffset = 2,\n        Router = 3,\n        TimeServer = 4,\n        NameServer = 5,\n        DomainNameServer = 6,\n        LogServer = 7,\n        CookieServer = 8,\n        LprServer = 9,\n        ImpressServer = 10,\n        ResourceLocationServer = 11,\n        HostName = 12,\n        BootFileSize = 13,\n        MeritDump = 14,\n        DomainName = 15,\n        SwapServer = 16,\n        RootPath = 17,\n        ExtensionPath = 18,\n        IpForwarding = 19,\n        NonLocalSourceRouting = 20,\n        PolicyFilter = 21,\n        MaximumDatagramReassemblySize = 22,\n        DefaultIpTtl = 23,\n        PathMtuAgingTimeout = 24,\n        PathMtuPlateauTable = 25,\n        InterfaceMtu = 26,\n        AllSubnetAreLocal = 27,\n        BroadcastAddress = 28,\n        PerformMaskDiscovery = 29,\n        MaskSupplier = 30,\n        PerformRouterDiscovery = 31,\n        RouterSolicitationAddress = 32,\n        StaticRoute = 33,\n        TrailerEncapsulation = 34,\n        ArpCacheTimeout = 35,\n        EthernetEncapsulation = 36,\n        TcpDefaultTtl = 37,\n        TcpKeepAliveInterval = 38,\n        TcpKeepAliveGarbage = 39,\n        NetworkInformationServiceDomain = 40,\n        NetworkInformationServers = 41,\n        NetworkTimeProtocolServers = 42,\n        VendorSpecificInformation = 43,\n        NetBiosOverTcpIpNameServer = 44,\n        NetBiosOverTcpIpDatagramDistributionServer = 45,\n        NetBiosOverTcpIpNodeType = 46,\n        NetBiosOverTcpIpScope = 47,\n        XWindowSystemFontServer = 48,\n        XWindowSystemDisplayManager = 49,\n        RequestedIpAddress = 50,\n        IpAddressLeaseTime = 51,\n        OptionOverload = 52,\n        DhcpMessageType = 53,\n        ServerIdentifier = 54,\n        ParameterRequestList = 55,\n        Message = 56,\n        MaximumDhcpMessageSize = 57,\n        RenewalTimeValue = 58,\n        RebindingTimeValue = 59,\n        VendorClassIdentifier = 60,\n        ClientIdentifier = 61,\n        NetworkInformationServicePlusDomain = 64,\n        NetworkInformationServicePlusServers = 65,\n        TftpServerName = 66,\n        BootfileName = 67,\n        MobileIpHomeAgent = 68,\n        SmtpServer = 69,\n        Pop3Server = 70,\n        NntpServer = 71,\n        DefaultWwwServer = 72,\n        DefaultFingerServer = 73,\n        DefaultIrc = 74,\n        StreetTalkServer = 75,\n        StreetTalkDirectoryAssistance = 76,\n        ClientFullyQualifiedDomainName = 81,\n        DomainSearch = 119,\n        ClasslessStaticRoute = 121,\n        CAPWAPAccessControllerAddresses = 138,\n        TftpServerAddress = 150,\n        End = 255\n    }\n\n    public class DhcpOption\n    {\n        #region variables\n\n        readonly DhcpOptionCode _code;\n        byte[] _value;\n\n        #endregion\n\n        #region constructor\n\n        public DhcpOption(DhcpOptionCode code, string hexValue)\n        {\n            ArgumentNullException.ThrowIfNull(hexValue);\n\n            _code = code;\n\n            if (hexValue.Contains(':'))\n                _value = hexValue.ParseColonHexString();\n            else\n                _value = Convert.FromHexString(hexValue);\n        }\n\n        public DhcpOption(DhcpOptionCode code, byte[] value)\n        {\n            ArgumentNullException.ThrowIfNull(value);\n\n            _code = code;\n            _value = value;\n        }\n\n        protected DhcpOption(DhcpOptionCode code, Stream s)\n        {\n            _code = code;\n\n            int len = s.ReadByte();\n            if (len < 0)\n                throw new EndOfStreamException();\n\n            _value = s.ReadExactly(len);\n        }\n\n        protected DhcpOption(DhcpOptionCode code)\n        {\n            _code = code;\n        }\n\n        #endregion\n\n        #region static\n\n        public static DhcpOption CreateEndOption()\n        {\n            return new DhcpOption(DhcpOptionCode.End);\n        }\n\n        public static DhcpOption Parse(Stream s)\n        {\n            int code = s.ReadByte();\n            if (code < 0)\n                throw new EndOfStreamException();\n\n            DhcpOptionCode optionCode = (DhcpOptionCode)code;\n\n            switch (optionCode)\n            {\n                case DhcpOptionCode.SubnetMask:\n                    return new SubnetMaskOption(s);\n\n                case DhcpOptionCode.Router:\n                    return new RouterOption(s);\n\n                case DhcpOptionCode.DomainNameServer:\n                    return new DomainNameServerOption(s);\n\n                case DhcpOptionCode.HostName:\n                    return new HostNameOption(s);\n\n                case DhcpOptionCode.DomainName:\n                    return new DomainNameOption(s);\n\n                case DhcpOptionCode.BroadcastAddress:\n                    return new BroadcastAddressOption(s);\n\n                case DhcpOptionCode.VendorSpecificInformation:\n                    return new VendorSpecificInformationOption(s);\n\n                case DhcpOptionCode.NetBiosOverTcpIpNameServer:\n                    return new NetBiosNameServerOption(s);\n\n                case DhcpOptionCode.RequestedIpAddress:\n                    return new RequestedIpAddressOption(s);\n\n                case DhcpOptionCode.IpAddressLeaseTime:\n                    return new IpAddressLeaseTimeOption(s);\n\n                case DhcpOptionCode.OptionOverload:\n                    return new OptionOverloadOption(s);\n\n                case DhcpOptionCode.DhcpMessageType:\n                    return new DhcpMessageTypeOption(s);\n\n                case DhcpOptionCode.ServerIdentifier:\n                    return new ServerIdentifierOption(s);\n\n                case DhcpOptionCode.ParameterRequestList:\n                    return new ParameterRequestListOption(s);\n\n                case DhcpOptionCode.MaximumDhcpMessageSize:\n                    return new MaximumDhcpMessageSizeOption(s);\n\n                case DhcpOptionCode.RenewalTimeValue:\n                    return new RenewalTimeValueOption(s);\n\n                case DhcpOptionCode.RebindingTimeValue:\n                    return new RebindingTimeValueOption(s);\n\n                case DhcpOptionCode.VendorClassIdentifier:\n                    return new VendorClassIdentifierOption(s);\n\n                case DhcpOptionCode.ClientIdentifier:\n                    return new ClientIdentifierOption(s);\n\n                case DhcpOptionCode.ClientFullyQualifiedDomainName:\n                    return new ClientFullyQualifiedDomainNameOption(s);\n\n                case DhcpOptionCode.DomainSearch:\n                    return new DomainSearchOption(s);\n\n                case DhcpOptionCode.ClasslessStaticRoute:\n                    return new ClasslessStaticRouteOption(s);\n\n                case DhcpOptionCode.CAPWAPAccessControllerAddresses:\n                    return new CAPWAPAccessControllerOption(s);\n\n                case DhcpOptionCode.TftpServerAddress:\n                    return new TftpServerAddressOption(s);\n\n                case DhcpOptionCode.Pad:\n                case DhcpOptionCode.End:\n                    return new DhcpOption(optionCode);\n\n                default:\n                    //unknown option\n                    return new DhcpOption(optionCode, s);\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        internal void AppendOptionValue(DhcpOption option)\n        {\n            byte[] value = new byte[_value.Length + option._value.Length];\n\n            Buffer.BlockCopy(_value, 0, value, 0, _value.Length);\n            Buffer.BlockCopy(option._value, 0, value, _value.Length, option._value.Length);\n\n            _value = value;\n        }\n\n        internal void ParseOptionValue()\n        {\n            if (_value != null)\n            {\n                using (MemoryStream mS = new MemoryStream(_value))\n                {\n                    ParseOptionValue(mS);\n                }\n            }\n        }\n\n        #endregion\n\n        #region protected\n\n        protected virtual void ParseOptionValue(Stream s)\n        { }\n\n        protected virtual void WriteOptionValue(Stream s)\n        {\n            if (_value == null)\n                throw new NotImplementedException();\n\n            s.Write(_value);\n        }\n\n        #endregion\n\n        #region public\n\n        public void WriteTo(Stream s)\n        {\n            switch (_code)\n            {\n                case DhcpOptionCode.Pad:\n                case DhcpOptionCode.End:\n                    s.WriteByte((byte)_code);\n                    break;\n\n                default:\n                    using (MemoryStream mS = new MemoryStream())\n                    {\n                        WriteOptionValue(mS);\n\n                        int len = 255;\n                        int valueLen = Convert.ToInt32(mS.Position);\n                        mS.Position = 0;\n\n                        do\n                        {\n                            if (valueLen < len)\n                                len = valueLen;\n\n                            //write option\n                            s.WriteByte((byte)_code); //code\n                            s.WriteByte((byte)len); //len\n                            mS.CopyTo(s, len, len); //value\n\n                            valueLen -= len;\n                        }\n                        while (valueLen > 0);\n                    }\n\n                    break;\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public DhcpOptionCode Code\n        { get { return _code; } }\n\n        public byte[] RawValue\n        { get { return _value; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/DhcpServer.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Dhcp.Options;\nusing DnsServerCore.Dns;\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dhcp\n{\n    //Dynamic Host Configuration Protocol\n    //https://datatracker.ietf.org/doc/html/rfc2131\n\n    //DHCP Options and BOOTP Vendor Extensions\n    //https://datatracker.ietf.org/doc/html/rfc2132\n\n    //Encoding Long Options in the Dynamic Host Configuration Protocol (DHCPv4)\n    //https://datatracker.ietf.org/doc/html/rfc3396\n\n    //Client Fully Qualified Domain Name(FQDN) Option\n    //https://datatracker.ietf.org/doc/html/rfc4702\n\n    public sealed class DhcpServer : IDisposable\n    {\n        #region enum\n\n        enum ServiceState\n        {\n            Stopped = 0,\n            Starting = 1,\n            Running = 2,\n            Stopping = 3\n        }\n\n        #endregion\n\n        #region variables\n\n        readonly string _scopesFolder;\n        readonly LogManager _log;\n\n        readonly ConcurrentDictionary<IPAddress, UdpListener> _udpListeners = new ConcurrentDictionary<IPAddress, UdpListener>();\n\n        readonly ConcurrentDictionary<string, Scope> _scopes = new ConcurrentDictionary<string, Scope>();\n\n        DnsServer _dnsServer;\n        AuthManager _authManager;\n\n        volatile ServiceState _state = ServiceState.Stopped;\n\n        readonly IPEndPoint _dhcpDefaultEP = new IPEndPoint(IPAddress.Any, 67);\n\n        Timer _maintenanceTimer;\n        const int MAINTENANCE_TIMER_INTERVAL = 10000;\n\n        DateTime _lastModifiedScopesSavedOn;\n\n        #endregion\n\n        #region constructor\n\n        public DhcpServer(string scopesFolder, LogManager log)\n        {\n            _scopesFolder = scopesFolder;\n            _log = log;\n\n            if (!Directory.Exists(_scopesFolder))\n            {\n                Directory.CreateDirectory(_scopesFolder);\n\n                //create default scope\n                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);\n                scope.Exclusions = new Exclusion[] { new Exclusion(IPAddress.Parse(\"192.168.1.1\"), IPAddress.Parse(\"192.168.1.10\")) };\n                scope.RouterAddress = IPAddress.Parse(\"192.168.1.1\");\n                scope.UseThisDnsServer = true;\n                scope.DomainName = \"home\";\n                scope.LeaseTimeDays = 1;\n                scope.IgnoreClientIdentifierOption = true;\n\n                SaveScopeFile(scope);\n            }\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _maintenanceTimer?.Dispose();\n\n            Stop();\n\n            if (_scopes is not null)\n            {\n                foreach (KeyValuePair<string, Scope> scope in _scopes)\n                    scope.Value.Dispose();\n\n                _scopes.Clear();\n            }\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task ReadUdpRequestAsync(Socket udpListener)\n        {\n            byte[] recvBuffer = new byte[1500];\n\n            try\n            {\n                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\n\n                EndPoint epAny = new IPEndPoint(IPAddress.Any, 0);\n\n                SocketReceiveMessageFromResult result;\n\n                while (true)\n                {\n                    try\n                    {\n                        result = await udpListener.ReceiveMessageFromAsync(recvBuffer, SocketFlags.None, epAny);\n                    }\n                    catch (SocketException ex)\n                    {\n                        switch (ex.SocketErrorCode)\n                        {\n                            case SocketError.ConnectionReset:\n                            case SocketError.HostUnreachable:\n                            case SocketError.NetworkReset:\n                                result = default;\n                                break;\n\n                            case SocketError.MessageSize:\n                                _log.Write(ex);\n\n                                result = default;\n                                break;\n\n                            default:\n                                throw;\n                        }\n                    }\n\n                    if (result.ReceivedBytes > 0)\n                    {\n                        if (processOnlyUnicastMessages && result.PacketInformation.Address.Equals(IPAddress.Broadcast))\n                            continue;\n\n                        try\n                        {\n                            DhcpMessage request = new DhcpMessage(new MemoryStream(recvBuffer, 0, result.ReceivedBytes, false));\n\n                            _ = ProcessDhcpRequestAsync(request, result.RemoteEndPoint as IPEndPoint, result.PacketInformation, udpListener);\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(result.RemoteEndPoint as IPEndPoint, ex);\n                        }\n                    }\n                }\n            }\n            catch (ObjectDisposedException)\n            {\n                //server stopped\n            }\n            catch (SocketException ex)\n            {\n                switch (ex.SocketErrorCode)\n                {\n                    case SocketError.OperationAborted:\n                    case SocketError.Interrupted:\n                        break; //server stopping\n\n                    default:\n                        if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                            return; //server stopping\n\n                        _log.Write(ex);\n                        break;\n                }\n            }\n            catch (Exception ex)\n            {\n                if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                    return; //server stopping\n\n                _log.Write(ex);\n            }\n        }\n\n        private async Task ProcessDhcpRequestAsync(DhcpMessage request, IPEndPoint remoteEP, IPPacketInformation ipPacketInformation, Socket udpListener)\n        {\n            try\n            {\n                DhcpMessage response = await ProcessDhcpMessageAsync(request, remoteEP, ipPacketInformation);\n\n                //send response\n                if (response != null)\n                {\n                    byte[] sendBuffer = new byte[1024];\n                    MemoryStream sendBufferStream = new MemoryStream(sendBuffer);\n\n                    response.WriteTo(sendBufferStream);\n\n                    //send dns datagram\n                    if (!request.RelayAgentIpAddress.Equals(IPAddress.Any))\n                    {\n                        //received request via relay agent so send unicast response to relay agent on port 67\n                        await udpListener.SendToAsync(new ArraySegment<byte>(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, new IPEndPoint(request.RelayAgentIpAddress, 67));\n                    }\n                    else if (!request.ClientIpAddress.Equals(IPAddress.Any))\n                    {\n                        //client is already configured and renewing lease so send unicast response on port 68\n                        await udpListener.SendToAsync(new ArraySegment<byte>(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, new IPEndPoint(request.ClientIpAddress, 68));\n                    }\n                    else\n                    {\n                        Socket udpSocket;\n\n                        //send response as broadcast on port 68 on appropriate interface bound socket\n                        if (_udpListeners.TryGetValue(response.ServerIdentifier.Address, out UdpListener listener))\n                            udpSocket = listener.Socket; //found scope specific socket\n                        else\n                            udpSocket = udpListener; //no appropriate socket found so use default socket\n\n                        await udpSocket.SendToAsync(new ArraySegment<byte>(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.DontRoute, new IPEndPoint(IPAddress.Broadcast, 68)); //no routing for broadcast\n                    }\n                }\n            }\n            catch (ObjectDisposedException)\n            {\n                //socket disposed\n            }\n            catch (Exception ex)\n            {\n                if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                    return; //server stopping\n\n                _log.Write(remoteEP, ex);\n            }\n        }\n\n        private async Task<DhcpMessage> ProcessDhcpMessageAsync(DhcpMessage request, IPEndPoint remoteEP, IPPacketInformation ipPacketInformation)\n        {\n            if (request.OpCode != DhcpMessageOpCode.BootRequest)\n                return null;\n\n            switch (request.DhcpMessageType?.Type)\n            {\n                case DhcpMessageType.Discover:\n                    {\n                        Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation);\n                        if (scope == null)\n                            return null; //no scope available; do nothing\n\n                        if ((request.ServerHostName != null) && (request.ServerHostName != scope.ServerHostName))\n                            return null; //discard request; since this request is for another server with the specified server host name\n\n                        if ((request.BootFileName != null) && (request.BootFileName != scope.BootFileName))\n                            return null; //discard request; since this request wants boot file not available on this server\n\n                        if (scope.OfferDelayTime > 0)\n                            await Task.Delay(scope.OfferDelayTime); //delay sending offer\n\n                        Lease offer = await scope.GetOfferAsync(request);\n                        if (offer == null)\n                            return null; //no offer available, do nothing\n\n                        IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress;\n                        string reservedLeaseHostName = null;\n\n                        if (!string.IsNullOrWhiteSpace(scope.DomainName))\n                        {\n                            //get override host name from reserved lease\n                            Lease reservedLease = scope.GetReservedLease(request);\n                            if (reservedLease is not null)\n                                reservedLeaseHostName = reservedLease.HostName;\n                        }\n\n                        List<DhcpOption> options = await scope.GetOptionsAsync(request, serverIdentifierAddress, reservedLeaseHostName, _dnsServer);\n                        if (options is null)\n                            return null;\n\n                        //log ip offer\n                        _log.Write(remoteEP, \"DHCP Server offered IP address [\" + offer.Address.ToString() + \"] to \" + request.GetClientFullIdentifier() + \" for scope: \" + scope.Name);\n\n                        return DhcpMessage.CreateReply(request, offer.Address, scope.ServerAddress ?? serverIdentifierAddress, scope.ServerHostName, scope.BootFileName, options);\n                    }\n\n                case DhcpMessageType.Request:\n                    {\n                        //request ip address lease or extend existing lease\n                        Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation);\n                        if (scope == null)\n                            return null; //no scope available; do nothing\n\n                        IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress;\n\n                        Lease leaseOffer;\n\n                        if (request.ServerIdentifier == null)\n                        {\n                            if (request.RequestedIpAddress == null)\n                            {\n                                //renewing or rebinding\n\n                                if (request.ClientIpAddress.Equals(IPAddress.Any))\n                                    return null; //client must set IP address in ciaddr; do nothing\n\n                                leaseOffer = scope.GetExistingLeaseOrOffer(request);\n                                if (leaseOffer == null)\n                                {\n                                    //no existing lease or offer available for client\n                                    //send nak\n                                    return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                                }\n\n                                if (!request.ClientIpAddress.Equals(leaseOffer.Address))\n                                {\n                                    //client ip is incorrect\n                                    //send nak\n                                    return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                                }\n                            }\n                            else\n                            {\n                                //init-reboot\n\n                                leaseOffer = scope.GetExistingLeaseOrOffer(request);\n                                if (leaseOffer == null)\n                                {\n                                    //no existing lease or offer available for client\n                                    //send nak\n                                    return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                                }\n\n                                if (!request.RequestedIpAddress.Address.Equals(leaseOffer.Address))\n                                {\n                                    //the client's notion of its IP address is not correct - RFC 2131\n                                    //send nak\n                                    return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                                }\n                            }\n\n                            if ((leaseOffer.Type == LeaseType.Dynamic) && (scope.IsAddressExcluded(leaseOffer.Address) || scope.IsAddressReserved(leaseOffer.Address)))\n                            {\n                                //client ip is excluded/reserved for dynamic allocations\n                                scope.ReleaseLease(leaseOffer);\n                                //send nak\n                                return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                            }\n\n                            Lease reservedLease = scope.GetReservedLease(request);\n                            if (reservedLease == null)\n                            {\n                                if (leaseOffer.Type == LeaseType.Reserved)\n                                {\n                                    //client's reserved lease has been removed so release the current lease and send NAK to allow it to get new allocation\n                                    scope.ReleaseLease(leaseOffer);\n                                    //send nak\n                                    return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                                }\n                            }\n                            else\n                            {\n                                if (!reservedLease.Address.Equals(leaseOffer.Address))\n                                {\n                                    //client has a new reserved lease so release the current lease and send NAK to allow it to get new allocation\n                                    scope.ReleaseLease(leaseOffer);\n                                    //send nak\n                                    return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                                }\n                            }\n                        }\n                        else\n                        {\n                            //selecting offer\n\n                            if (request.RequestedIpAddress == null)\n                                return null; //client MUST include this option; do nothing\n\n                            if (!request.ServerIdentifier.Address.Equals(serverIdentifierAddress))\n                                return null; //offer declined by client; do nothing\n\n                            leaseOffer = scope.GetExistingLeaseOrOffer(request);\n                            if (leaseOffer == null)\n                            {\n                                //no existing lease or offer available for client\n                                //send nak\n                                return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                            }\n\n                            if (!request.RequestedIpAddress.Address.Equals(leaseOffer.Address))\n                            {\n                                //requested ip is incorrect\n                                //send nak\n                                return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() });\n                            }\n                        }\n\n                        string reservedLeaseHostName = null;\n\n                        if (!string.IsNullOrWhiteSpace(scope.DomainName))\n                        {\n                            //get override host name from reserved lease\n                            Lease reservedLease = scope.GetReservedLease(request);\n                            if (reservedLease is not null)\n                                reservedLeaseHostName = reservedLease.HostName;\n                        }\n\n                        List<DhcpOption> options = await scope.GetOptionsAsync(request, serverIdentifierAddress, reservedLeaseHostName, _dnsServer);\n                        if (options is null)\n                            return null;\n\n                        scope.CommitLease(leaseOffer);\n\n                        //log ip lease\n                        _log.Write(remoteEP, \"DHCP Server leased IP address [\" + leaseOffer.Address.ToString() + \"] to \" + request.GetClientFullIdentifier() + \" for scope: \" + scope.Name);\n\n                        if (string.IsNullOrWhiteSpace(scope.DomainName))\n                        {\n                            //update lease hostname\n                            leaseOffer.SetHostName(request.HostName?.HostName);\n                        }\n                        else\n                        {\n                            //update dns\n                            string clientDomainName = null;\n\n                            if (!string.IsNullOrWhiteSpace(reservedLeaseHostName))\n                                clientDomainName = GetSanitizedHostName(reservedLeaseHostName) + \".\" + scope.DomainName;\n\n                            if (string.IsNullOrWhiteSpace(clientDomainName))\n                            {\n                                foreach (DhcpOption option in options)\n                                {\n                                    if (option.Code == DhcpOptionCode.ClientFullyQualifiedDomainName)\n                                    {\n                                        clientDomainName = (option as ClientFullyQualifiedDomainNameOption).DomainName;\n                                        break;\n                                    }\n                                }\n                            }\n\n                            if (string.IsNullOrWhiteSpace(clientDomainName))\n                            {\n                                if ((request.HostName is not null) && !string.IsNullOrWhiteSpace(request.HostName.HostName))\n                                    clientDomainName = GetSanitizedHostName(request.HostName.HostName) + \".\" + scope.DomainName;\n                            }\n\n                            if (!string.IsNullOrWhiteSpace(clientDomainName))\n                            {\n                                if (!clientDomainName.Equals(leaseOffer.HostName, StringComparison.OrdinalIgnoreCase))\n                                    UpdateDnsAuthZone(false, scope, leaseOffer); //hostname changed! delete old hostname entry from DNS\n\n                                leaseOffer.SetHostName(clientDomainName);\n                                UpdateDnsAuthZone(true, scope, leaseOffer);\n                            }\n                        }\n\n                        return DhcpMessage.CreateReply(request, leaseOffer.Address, scope.ServerAddress ?? serverIdentifierAddress, scope.ServerHostName, scope.BootFileName, options);\n                    }\n\n                case DhcpMessageType.Decline:\n                    {\n                        //ip address is already in use as detected by client via ARP\n\n                        if ((request.ServerIdentifier == null) || (request.RequestedIpAddress == null))\n                            return null; //client MUST include these option; do nothing\n\n                        Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation);\n                        if (scope == null)\n                            return null; //no scope available; do nothing\n\n                        IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress;\n\n                        if (!request.ServerIdentifier.Address.Equals(serverIdentifierAddress))\n                            return null; //request not for this server; do nothing\n\n                        Lease lease = scope.GetExistingLeaseOrOffer(request);\n                        if (lease == null)\n                            return null; //no existing lease or offer available for client; do nothing\n\n                        if (!lease.Address.Equals(request.RequestedIpAddress.Address))\n                            return null; //the client's notion of its IP address is not correct; do nothing\n\n                        //remove lease since the IP address is used by someone else\n                        scope.ReleaseLease(lease);\n\n                        //log issue\n                        _log.Write(remoteEP, \"DHCP Server received DECLINE message for scope '\" + scope.Name + \"': \" + lease.GetClientInfo() + \" detected that IP address [\" + lease.Address + \"] is already in use.\");\n\n                        //update dns\n                        UpdateDnsAuthZone(false, scope, lease);\n\n                        //do nothing\n                        return null;\n                    }\n\n                case DhcpMessageType.Release:\n                    {\n                        //cancel ip address lease\n\n                        if (request.ServerIdentifier == null)\n                            return null; //client MUST include this option; do nothing\n\n                        Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation);\n                        if (scope == null)\n                            return null; //no scope available; do nothing\n\n                        IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress;\n\n                        if (!request.ServerIdentifier.Address.Equals(serverIdentifierAddress))\n                            return null; //request not for this server; do nothing\n\n                        Lease lease = scope.GetExistingLeaseOrOffer(request);\n                        if (lease == null)\n                            return null; //no existing lease or offer available for client; do nothing\n\n                        if (!lease.Address.Equals(request.ClientIpAddress))\n                            return null; //the client's notion of its IP address is not correct; do nothing\n\n                        //release lease\n                        scope.ReleaseLease(lease);\n\n                        //log ip lease release\n                        _log.Write(remoteEP, \"DHCP Server released IP address [\" + lease.Address.ToString() + \"] that was leased to \" + lease.GetClientInfo() + \" for scope: \" + scope.Name);\n\n                        //update dns\n                        UpdateDnsAuthZone(false, scope, lease);\n\n                        //do nothing\n                        return null;\n                    }\n\n                case DhcpMessageType.Inform:\n                    {\n                        //need only local config; already has ip address assigned externally/manually\n\n                        Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation);\n                        if (scope == null)\n                            return null; //no scope available; do nothing\n\n                        IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress;\n\n                        //log inform\n                        _log.Write(remoteEP, \"DHCP Server received INFORM message from \" + request.GetClientFullIdentifier() + \" for scope: \" + scope.Name);\n\n                        List<DhcpOption> options = await scope.GetOptionsAsync(request, serverIdentifierAddress, null, _dnsServer);\n                        if (options is null)\n                            return null;\n\n                        if (!string.IsNullOrWhiteSpace(scope.DomainName))\n                        {\n                            //update dns\n                            string clientDomainName = null;\n\n                            foreach (DhcpOption option in options)\n                            {\n                                if (option.Code == DhcpOptionCode.ClientFullyQualifiedDomainName)\n                                {\n                                    clientDomainName = (option as ClientFullyQualifiedDomainNameOption).DomainName;\n                                    break;\n                                }\n                            }\n\n                            if (string.IsNullOrWhiteSpace(clientDomainName))\n                            {\n                                if (request.HostName is not null)\n                                    clientDomainName = GetSanitizedHostName(request.HostName.HostName) + \".\" + scope.DomainName;\n                            }\n\n                            if (!string.IsNullOrWhiteSpace(clientDomainName))\n                                UpdateDnsAuthZone(true, scope, clientDomainName, request.ClientIpAddress, false);\n                        }\n\n                        return DhcpMessage.CreateReply(request, IPAddress.Any, scope.ServerAddress ?? serverIdentifierAddress, null, null, options);\n                    }\n\n                default:\n                    return null;\n            }\n        }\n\n        private Scope FindScope(DhcpMessage request, IPAddress remoteAddress, IPPacketInformation ipPacketInformation)\n        {\n            if (request.RelayAgentIpAddress.Equals(IPAddress.Any))\n            {\n                //no relay agent\n                if (request.ClientIpAddress.Equals(IPAddress.Any))\n                {\n                    if (!ipPacketInformation.Address.Equals(IPAddress.Broadcast))\n                        return null; //message destination address must be broadcast address\n\n                    //broadcast request\n                    Scope foundScope = null;\n\n                    foreach (KeyValuePair<string, Scope> entry in _scopes)\n                    {\n                        Scope scope = entry.Value;\n\n                        if (scope.Enabled && (scope.InterfaceIndex == ipPacketInformation.Interface))\n                        {\n                            if (scope.GetReservedLease(request) != null)\n                                return scope; //found reserved lease on this scope\n\n                            if ((foundScope == null) && !scope.AllowOnlyReservedLeases)\n                                foundScope = scope;\n                        }\n                    }\n\n                    return foundScope;\n                }\n                else\n                {\n                    if ((request.DhcpMessageType?.Type != DhcpMessageType.Decline) && !remoteAddress.Equals(request.ClientIpAddress))\n                        return null; //client ip must match udp src addr\n\n                    //unicast request\n                    foreach (KeyValuePair<string, Scope> entry in _scopes)\n                    {\n                        Scope scope = entry.Value;\n\n                        if (scope.Enabled && scope.IsAddressInRange(request.ClientIpAddress))\n                            return scope;\n                    }\n\n                    return null;\n                }\n            }\n            else\n            {\n                //relay agent unicast\n                Scope foundScope = null;\n\n                foreach (KeyValuePair<string, Scope> entry in _scopes)\n                {\n                    Scope scope = entry.Value;\n\n                    if (scope.Enabled && scope.InterfaceAddress.Equals(IPAddress.Any) && scope.IsAddressInNetwork(request.RelayAgentIpAddress))\n                    {\n                        if (scope.GetReservedLease(request) != null)\n                            return scope; //found reserved lease on this scope\n\n                        if (!request.ClientIpAddress.Equals(IPAddress.Any) && scope.IsAddressInRange(request.ClientIpAddress))\n                            return scope; //client IP address is in scope range\n\n                        if ((foundScope == null) && !scope.AllowOnlyReservedLeases)\n                            foundScope = scope;\n                    }\n                }\n\n                return foundScope;\n            }\n        }\n\n        internal static string GetSanitizedHostName(string hostname)\n        {\n            StringBuilder sb = new StringBuilder(hostname.Length);\n\n            foreach (char c in hostname)\n            {\n                if ((c >= 97) && (c <= 122)) //[a-z]\n                    sb.Append(c);\n                else if ((c >= 65) && (c <= 90)) //[A-Z]\n                    sb.Append(c);\n                else if ((c >= 48) && (c <= 57)) //[0-9]\n                    sb.Append(c);\n                else if (c == 45) //[-]\n                    sb.Append(c);\n                else if (c == 95) //[_]\n                    sb.Append(c);\n                else if (c == '.')\n                    sb.Append(c);\n                else if (c == ' ')\n                    sb.Append('-');\n            }\n\n            return sb.ToString();\n        }\n\n        internal void UpdateDnsAuthZone(bool add, Scope scope, Lease lease)\n        {\n            UpdateDnsAuthZone(add, scope, lease.HostName, lease.Address, lease.Type == LeaseType.Reserved);\n        }\n\n        private void UpdateDnsAuthZone(bool add, Scope scope, string domain, IPAddress address, bool isReservedLease)\n        {\n            if ((_dnsServer is null) || (_authManager is null))\n                return;\n\n            if (string.IsNullOrWhiteSpace(scope.DomainName) || !scope.DnsUpdates)\n                return;\n\n            if (string.IsNullOrWhiteSpace(domain))\n                return;\n\n            if (!DnsClient.IsDomainNameValid(domain))\n                return;\n\n            if (!domain.EndsWith(\".\" + scope.DomainName, StringComparison.OrdinalIgnoreCase))\n                return; //domain does not end with scope domain name\n\n            try\n            {\n                string zoneName = null;\n                string reverseDomain = Zone.GetReverseZone(address, 32);\n                string reverseZoneName = null;\n\n                if (add)\n                {\n                    //update forward zone\n                    AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(scope.DomainName);\n                    if (zoneInfo is null)\n                    {\n                        //zone does not exists; create new primary zone\n                        zoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(scope.DomainName);\n                        if (zoneInfo is null)\n                        {\n                            _log.Write(\"DHCP Server failed to create DNS primary zone '\" + scope.DomainName + \"'.\");\n                            return;\n                        }\n\n                        //set permissions\n                        _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SaveConfigFile();\n\n                        _log.Write(\"DHCP Server create DNS primary zone '\" + zoneInfo.DisplayName + \"'.\");\n                    }\n                    else if ((zoneInfo.Type != AuthZoneType.Primary) && (zoneInfo.Type != AuthZoneType.Forwarder))\n                    {\n                        if (zoneInfo.Name.Equals(scope.DomainName, StringComparison.OrdinalIgnoreCase))\n                            throw new DhcpServerException(\"Cannot update DNS zone '\" + zoneInfo.DisplayName + \"': not a primary or a forwarder zone.\");\n\n                        //create new primary zone\n                        zoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(scope.DomainName);\n                        if (zoneInfo is null)\n                        {\n                            _log.Write(\"DHCP Server failed to create DNS primary zone '\" + scope.DomainName + \"'.\");\n                            return;\n                        }\n\n                        //set permissions\n                        _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SaveConfigFile();\n\n                        _log.Write(\"DHCP Server create DNS primary zone '\" + zoneInfo.DisplayName + \"'.\");\n                    }\n\n                    zoneName = zoneInfo.Name;\n\n                    if (!isReservedLease && !scope.DnsOverwriteForDynamicLease)\n                    {\n                        //check for existing record for the dynamic leases\n                        IReadOnlyList<DnsResourceRecord> existingRecords = _dnsServer.AuthZoneManager.GetRecords(zoneName, domain, DnsResourceRecordType.A);\n                        if (existingRecords.Count > 0)\n                        {\n                            foreach (DnsResourceRecord existingRecord in existingRecords)\n                            {\n                                IPAddress existingAddress = (existingRecord.RDATA as DnsARecordData).Address;\n                                if (!existingAddress.Equals(address))\n                                {\n                                    //a DNS record already exists for the specified domain name with a different address\n                                    //do not change DNS record for this dynamic lease\n                                    _log.Write(\"DHCP Server cannot update DNS: an A record already exists for '\" + domain + \"' with a different IP address [\" + existingAddress.ToString() + \"].\");\n                                    return;\n                                }\n                            }\n                        }\n                    }\n\n                    DnsResourceRecord aRecord = new DnsResourceRecord(domain, DnsResourceRecordType.A, DnsClass.IN, scope.DnsTtl, new DnsARecordData(address));\n\n                    GenericRecordInfo aRecordInfo = aRecord.GetAuthGenericRecordInfo();\n                    aRecordInfo.LastModified = DateTime.UtcNow;\n                    aRecordInfo.ExpiryTtl = scope.GetLeaseTime();\n                    aRecordInfo.Comments = $\"Via '{scope.Name}' DHCP scope\";\n\n                    _dnsServer.AuthZoneManager.SetRecord(zoneName, aRecord);\n                    _log.Write(\"DHCP Server updated DNS A record '\" + domain + \"' with IP address [\" + address.ToString() + \"].\");\n\n                    //update reverse zone\n                    AuthZoneInfo reverseZoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(reverseDomain);\n                    if (reverseZoneInfo is null)\n                    {\n                        string reverseZone = Zone.GetReverseZone(address, scope.SubnetMask);\n\n                        //reverse zone does not exists; create new reverse primary zone\n                        reverseZoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(reverseZone);\n                        if (reverseZoneInfo is null)\n                        {\n                            _log.Write(\"DHCP Server failed to create DNS primary zone '\" + reverseZone + \"'.\");\n                            return;\n                        }\n\n                        //set permissions\n                        _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SaveConfigFile();\n\n                        _log.Write(\"DHCP Server create DNS primary zone '\" + reverseZoneInfo.DisplayName + \"'.\");\n                    }\n                    else if ((reverseZoneInfo.Type != AuthZoneType.Primary) && (reverseZoneInfo.Type != AuthZoneType.Forwarder))\n                    {\n                        string reverseZone = Zone.GetReverseZone(address, scope.SubnetMask);\n\n                        if (reverseZoneInfo.Name.Equals(reverseZone, StringComparison.OrdinalIgnoreCase))\n                            throw new DhcpServerException(\"Cannot update reverse DNS zone '\" + reverseZoneInfo.DisplayName + \"': not a primary or a forwarder zone.\");\n\n                        //create new reverse primary zone\n                        reverseZoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(reverseZone);\n                        if (reverseZoneInfo is null)\n                        {\n                            _log.Write(\"DHCP Server failed to create DNS primary zone '\" + reverseZone + \"'.\");\n                            return;\n                        }\n\n                        //set permissions\n                        _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _authManager.SaveConfigFile();\n\n                        _log.Write(\"DHCP Server create DNS primary zone '\" + reverseZoneInfo.DisplayName + \"'.\");\n                    }\n\n                    reverseZoneName = reverseZoneInfo.Name;\n\n                    DnsResourceRecord ptrRecord = new DnsResourceRecord(reverseDomain, DnsResourceRecordType.PTR, DnsClass.IN, scope.DnsTtl, new DnsPTRRecordData(domain));\n\n                    GenericRecordInfo ptrRecordInfo = aRecord.GetAuthGenericRecordInfo();\n                    ptrRecordInfo.LastModified = DateTime.UtcNow;\n                    ptrRecordInfo.ExpiryTtl = scope.GetLeaseTime();\n                    ptrRecordInfo.Comments = $\"Via '{scope.Name}' DHCP scope\";\n\n                    _dnsServer.AuthZoneManager.SetRecord(reverseZoneName, ptrRecord);\n\n                    _log.Write(\"DHCP Server updated DNS PTR record '\" + reverseDomain + \"' with domain name '\" + domain + \"'.\");\n                }\n                else\n                {\n                    //remove from forward zone\n                    AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(domain);\n                    if ((zoneInfo is not null) && ((zoneInfo.Type == AuthZoneType.Primary) || (zoneInfo.Type == AuthZoneType.Forwarder)))\n                    {\n                        //primary zone exists\n                        zoneName = zoneInfo.Name;\n                        _dnsServer.AuthZoneManager.DeleteRecord(zoneName, domain, DnsResourceRecordType.A, new DnsARecordData(address));\n                        _log.Write(\"DHCP Server deleted DNS A record '\" + domain + \"' with address [\" + address.ToString() + \"].\");\n                    }\n\n                    //remove from reverse zone\n                    AuthZoneInfo reverseZoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(reverseDomain);\n                    if ((reverseZoneInfo != null) && ((reverseZoneInfo.Type == AuthZoneType.Primary) || (reverseZoneInfo.Type == AuthZoneType.Forwarder)))\n                    {\n                        //primary reverse zone exists\n                        reverseZoneName = reverseZoneInfo.Name;\n                        _dnsServer.AuthZoneManager.DeleteRecord(reverseZoneName, reverseDomain, DnsResourceRecordType.PTR, new DnsPTRRecordData(domain));\n                        _log.Write(\"DHCP Server deleted DNS PTR record '\" + reverseDomain + \"' with domain '\" + domain + \"'.\");\n                    }\n                }\n\n                //save auth zone file\n                if (zoneName is not null)\n                    _dnsServer?.AuthZoneManager.SaveZoneFile(zoneName);\n\n                //save reverse auth zone file\n                if (reverseZoneName is not null)\n                    _dnsServer?.AuthZoneManager.SaveZoneFile(reverseZoneName);\n            }\n            catch (Exception ex)\n            {\n                _log.Write(ex);\n            }\n        }\n\n        private void BindUdpListener(IPEndPoint dhcpEP)\n        {\n            UdpListener listener = _udpListeners.GetOrAdd(dhcpEP.Address, delegate (IPAddress key)\n            {\n                Socket udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);\n\n                try\n                {\n                    #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom()\n\n                    if (Environment.OSVersion.Platform == PlatformID.Win32NT)\n                    {\n                        const uint IOC_IN = 0x80000000;\n                        const uint IOC_VENDOR = 0x18000000;\n                        const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;\n\n                        udpSocket.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);\n                    }\n\n                    #endregion\n\n                    //bind to interface address\n                    if (Environment.OSVersion.Platform == PlatformID.Unix)\n                        udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses\n\n                    udpSocket.EnableBroadcast = true;\n                    udpSocket.ExclusiveAddressUse = false;\n\n                    udpSocket.Bind(dhcpEP);\n\n                    //start reading dhcp packets\n                    _ = Task.Factory.StartNew(delegate ()\n                    {\n                        return ReadUdpRequestAsync(udpSocket);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current);\n\n                    return new UdpListener(udpSocket);\n                }\n                catch\n                {\n                    udpSocket.Dispose();\n                    throw;\n                }\n            });\n\n            listener.IncrementScopeCount();\n        }\n\n        private bool UnbindUdpListener(IPEndPoint dhcpEP)\n        {\n            if (_udpListeners.TryGetValue(dhcpEP.Address, out UdpListener listener))\n            {\n                listener.DecrementScopeCount();\n\n                if (listener.ScopeCount < 1)\n                {\n                    if (_udpListeners.TryRemove(dhcpEP.Address, out _))\n                    {\n                        listener.Socket.Dispose();\n                        return true;\n                    }\n                }\n            }\n\n            return false;\n        }\n\n        private async Task<bool> ActivateScopeAsync(Scope scope, bool waitForInterface, bool throwException = false)\n        {\n            IPEndPoint dhcpEP = null;\n\n            try\n            {\n                //find scope interface for binding socket\n                if (waitForInterface)\n                {\n                    //retry for 30 seconds for interface to come up\n                    int tries = 0;\n                    while (true)\n                    {\n                        if (scope.FindInterface())\n                        {\n                            if (!scope.InterfaceAddress.Equals(IPAddress.Any))\n                                break; //break only when specific interface address is found\n                        }\n\n                        if (++tries >= 30)\n                        {\n                            if (scope.InterfaceAddress == null)\n                                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.\");\n\n                            break; //use the available ANY interface address\n                        }\n\n                        await Task.Delay(1000);\n                    }\n                }\n                else\n                {\n                    if (!scope.FindInterface())\n                        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.\");\n                }\n\n                //find this dns server address in case the network config has changed\n                if (scope.UseThisDnsServer)\n                    scope.FindThisDnsServerAddress();\n\n                dhcpEP = new IPEndPoint(scope.InterfaceAddress, 67);\n\n                if (!dhcpEP.Address.Equals(IPAddress.Any))\n                {\n                    int tries = 0;\n\n                    do\n                    {\n                        try\n                        {\n                            BindUdpListener(dhcpEP);\n                            break;\n                        }\n                        catch\n                        {\n                            if (!waitForInterface || (++tries >= 3))\n                                throw;\n\n                            await Task.Delay(5000);\n                        }\n                    }\n                    while (waitForInterface);\n                }\n\n                try\n                {\n                    BindUdpListener(_dhcpDefaultEP);\n                }\n                catch\n                {\n                    if (!dhcpEP.Address.Equals(IPAddress.Any))\n                        UnbindUdpListener(dhcpEP);\n\n                    throw;\n                }\n\n                if (_dnsServer is not null)\n                {\n                    //update valid leases into dns\n                    DateTime utcNow = DateTime.UtcNow;\n\n                    foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in scope.Leases)\n                        UpdateDnsAuthZone(utcNow < lease.Value.LeaseExpires, scope, lease.Value); //lease valid\n                }\n\n                _log.Write(dhcpEP, \"DHCP Server successfully activated scope: \" + scope.Name);\n\n                return true;\n            }\n            catch (Exception ex)\n            {\n                _log.Write(dhcpEP, \"DHCP Server failed to activate scope: \" + scope.Name + \"\\r\\n\" + ex.ToString());\n\n                if (throwException)\n                    throw;\n            }\n\n            return false;\n        }\n\n        private bool DeactivateScope(Scope scope, bool throwException = false)\n        {\n            IPEndPoint dhcpEP = null;\n\n            try\n            {\n                IPAddress interfaceAddress = scope.InterfaceAddress;\n                dhcpEP = new IPEndPoint(interfaceAddress, 67);\n\n                if (!interfaceAddress.Equals(IPAddress.Any))\n                    UnbindUdpListener(dhcpEP);\n\n                UnbindUdpListener(_dhcpDefaultEP);\n\n                if (_dnsServer is not null)\n                {\n                    //remove all leases from dns\n                    foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in scope.Leases)\n                        UpdateDnsAuthZone(false, scope, lease.Value);\n                }\n\n                _log.Write(dhcpEP, \"DHCP Server successfully deactivated scope: \" + scope.Name);\n\n                return true;\n            }\n            catch (Exception ex)\n            {\n                _log.Write(dhcpEP, \"DHCP Server failed to deactivate scope: \" + scope.Name + \"\\r\\n\" + ex.ToString());\n\n                if (throwException)\n                    throw;\n            }\n\n            return false;\n        }\n\n        private async Task LoadScopeAsync(Scope scope, bool waitForInterface)\n        {\n            foreach (KeyValuePair<string, Scope> entry in _scopes)\n            {\n                Scope existingScope = entry.Value;\n\n                if (existingScope.IsAddressInRange(scope.StartingAddress) || existingScope.IsAddressInRange(scope.EndingAddress))\n                    throw new DhcpServerException(\"Scope with overlapping range already exists: \" + existingScope.StartingAddress.ToString() + \"-\" + existingScope.EndingAddress.ToString());\n            }\n\n            if (!_scopes.TryAdd(scope.Name, scope))\n                throw new DhcpServerException(\"Scope with same name already exists.\");\n\n            if (scope.Enabled)\n            {\n                if (!await ActivateScopeAsync(scope, waitForInterface))\n                    scope.SetEnabled(false);\n            }\n\n            _log.Write(\"DHCP Server successfully loaded scope: \" + scope.Name);\n        }\n\n        private void UnloadScope(Scope scope)\n        {\n            if (scope.Enabled)\n                DeactivateScope(scope);\n\n            if (_scopes.TryRemove(scope.Name, out Scope removedScope))\n            {\n                removedScope.Dispose();\n\n                _log.Write(\"DHCP Server successfully unloaded scope: \" + scope.Name);\n            }\n        }\n\n        private void LoadAllScopeFiles()\n        {\n            string[] scopeFiles = Directory.GetFiles(_scopesFolder, \"*.scope\");\n\n            foreach (string scopeFile in scopeFiles)\n                _ = LoadScopeFileAsync(scopeFile);\n\n            _lastModifiedScopesSavedOn = DateTime.UtcNow;\n        }\n\n        private async Task LoadScopeFileAsync(string scopeFile)\n        {\n            //load scope file async to allow waiting for interface to come up\n            try\n            {\n                using (FileStream fS = new FileStream(scopeFile, FileMode.Open, FileAccess.Read))\n                {\n                    await LoadScopeAsync(new Scope(fS, _log, this), true);\n                }\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DHCP Server failed to load scope file: \" + scopeFile + \"\\r\\n\" + ex.ToString());\n            }\n        }\n\n        private void SaveScopeFile(Scope scope)\n        {\n            string scopeFile = Path.Combine(_scopesFolder, scope.Name + \".scope\");\n\n            try\n            {\n                using (MemoryStream mS = new MemoryStream())\n                {\n                    //serialize scope\n                    scope.WriteTo(mS);\n\n                    //write config\n                    mS.Position = 0;\n\n                    using (FileStream fS = new FileStream(scopeFile, FileMode.Create, FileAccess.Write))\n                    {\n                        mS.CopyTo(fS);\n                    }\n                }\n\n                _log.Write(\"DHCP Server successfully saved scope file: \" + scopeFile);\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DHCP Server failed to save scope file: \" + scopeFile + \"\\r\\n\" + ex.ToString());\n            }\n        }\n\n        private void DeleteScopeFile(string scopeName)\n        {\n            string scopeFile = Path.Combine(_scopesFolder, scopeName + \".scope\");\n\n            try\n            {\n                File.Delete(scopeFile);\n\n                _log.Write(\"DHCP Server successfully deleted scope file: \" + scopeFile);\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DHCP Server failed to delete scope file: \" + scopeFile + \"\\r\\n\" + ex.ToString());\n            }\n        }\n\n        private void SaveModifiedScopes()\n        {\n            DateTime currentDateTime = DateTime.UtcNow;\n\n            foreach (KeyValuePair<string, Scope> scope in _scopes)\n            {\n                if (scope.Value.LastModified > _lastModifiedScopesSavedOn)\n                    SaveScopeFile(scope.Value);\n            }\n\n            _lastModifiedScopesSavedOn = currentDateTime;\n        }\n\n        private void StartMaintenanceTimer()\n        {\n            if (_maintenanceTimer == null)\n            {\n                _maintenanceTimer = new Timer(delegate (object state)\n                {\n                    try\n                    {\n                        foreach (KeyValuePair<string, Scope> scope in _scopes)\n                        {\n                            scope.Value.RemoveExpiredOffers();\n\n                            List<Lease> expiredLeases = scope.Value.RemoveExpiredLeases();\n                            if (expiredLeases.Count > 0)\n                            {\n                                _log.Write(\"DHCP Server removed \" + expiredLeases.Count + \" lease(s) from scope: \" + scope.Value.Name);\n\n                                foreach (Lease expiredLease in expiredLeases)\n                                    UpdateDnsAuthZone(false, scope.Value, expiredLease);\n                            }\n                        }\n\n                        SaveModifiedScopes();\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(ex);\n                    }\n                    finally\n                    {\n                        try\n                        {\n                            _maintenanceTimer?.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);\n                        }\n                        catch (ObjectDisposedException)\n                        { }\n                    }\n                }, null, Timeout.Infinite, Timeout.Infinite);\n            }\n\n            _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);\n        }\n\n        private void StopMaintenanceTimer()\n        {\n            _maintenanceTimer.Change(Timeout.Infinite, Timeout.Infinite);\n        }\n\n        #endregion\n\n        #region public\n\n        public void Start()\n        {\n            if (_disposed)\n                ObjectDisposedException.ThrowIf(_disposed, this);\n\n            if (_state != ServiceState.Stopped)\n                throw new InvalidOperationException(\"DHCP Server is already running.\");\n\n            _state = ServiceState.Starting;\n\n            LoadAllScopeFiles();\n            StartMaintenanceTimer();\n\n            _state = ServiceState.Running;\n        }\n\n        public void Stop()\n        {\n            if (_state != ServiceState.Running)\n                return;\n\n            _state = ServiceState.Stopping;\n\n            StopMaintenanceTimer();\n\n            SaveModifiedScopes();\n\n            foreach (KeyValuePair<string, Scope> scope in _scopes)\n                UnloadScope(scope.Value);\n\n            _udpListeners.Clear();\n\n            _state = ServiceState.Stopped;\n        }\n\n        public async Task AddScopeAsync(Scope scope)\n        {\n            await LoadScopeAsync(scope, false);\n            SaveScopeFile(scope);\n        }\n\n        public Scope GetScope(string name)\n        {\n            if (_scopes.TryGetValue(name, out Scope scope))\n                return scope;\n\n            return null;\n        }\n\n        public void RenameScope(string oldName, string newName)\n        {\n            Scope.ValidateScopeName(newName);\n\n            if (!_scopes.TryGetValue(oldName, out Scope scope))\n                throw new DhcpServerException(\"Scope with name '\" + oldName + \"' does not exists.\");\n\n            if (!_scopes.TryAdd(newName, scope))\n                throw new DhcpServerException(\"Scope with name '\" + newName + \"' already exists.\");\n\n            scope.Name = newName;\n            _scopes.TryRemove(oldName, out _);\n\n            SaveScopeFile(scope);\n            DeleteScopeFile(oldName);\n        }\n\n        public void DeleteScope(string name)\n        {\n            if (_scopes.TryGetValue(name, out Scope scope))\n            {\n                UnloadScope(scope);\n                DeleteScopeFile(scope.Name);\n            }\n        }\n\n        public async Task<bool> EnableScopeAsync(string name, bool throwException = false)\n        {\n            if (_scopes.TryGetValue(name, out Scope scope))\n            {\n                if (!scope.Enabled && await ActivateScopeAsync(scope, false, throwException))\n                {\n                    scope.SetEnabled(true);\n                    SaveScopeFile(scope);\n\n                    return true;\n                }\n            }\n\n            return false;\n        }\n\n        public bool DisableScope(string name, bool throwException = false)\n        {\n            if (_scopes.TryGetValue(name, out Scope scope))\n            {\n                if (scope.Enabled && DeactivateScope(scope, throwException))\n                {\n                    scope.SetEnabled(false);\n                    SaveScopeFile(scope);\n\n                    return true;\n                }\n            }\n\n            return false;\n        }\n\n        public void SaveScope(string name)\n        {\n            if (_scopes.TryGetValue(name, out Scope scope))\n                SaveScopeFile(scope);\n        }\n\n        public IDictionary<string, string> GetAddressHostNameMap()\n        {\n            Dictionary<string, string> map = new Dictionary<string, string>();\n\n            foreach (KeyValuePair<string, Scope> scope in _scopes)\n            {\n                foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in scope.Value.Leases)\n                {\n                    if (!string.IsNullOrEmpty(lease.Value.HostName))\n                        map.Add(lease.Value.Address.ToString(), lease.Value.HostName);\n                }\n            }\n\n            return map;\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyDictionary<string, Scope> Scopes\n        { get { return _scopes; } }\n\n        public DnsServer DnsServer\n        {\n            get { return _dnsServer; }\n            set { _dnsServer = value; }\n        }\n\n        internal AuthManager AuthManager\n        {\n            get { return _authManager; }\n            set { _authManager = value; }\n        }\n\n        #endregion\n\n        class UdpListener\n        {\n            #region private\n\n            readonly Socket _socket;\n            volatile int _scopeCount;\n\n            #endregion\n\n            #region constructor\n\n            public UdpListener(Socket socket)\n            {\n                _socket = socket;\n            }\n\n            #endregion\n\n            #region public\n\n            public void IncrementScopeCount()\n            {\n                Interlocked.Increment(ref _scopeCount);\n            }\n\n            public void DecrementScopeCount()\n            {\n                Interlocked.Decrement(ref _scopeCount);\n            }\n\n            #endregion\n\n            #region properties\n\n            public Socket Socket\n            { get { return _socket; } }\n\n            public int ScopeCount\n            { get { return _scopeCount; } }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/DhcpServerException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore.Dhcp\n{\n    public class DhcpServerException : Exception\n    {\n        #region constructors\n\n        public DhcpServerException()\n            : base()\n        { }\n\n        public DhcpServerException(string message)\n            : base(message)\n        { }\n\n        public DhcpServerException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Exclusion.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Net;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerCore.Dhcp\n{\n    public class Exclusion\n    {\n        #region variables\n\n        readonly IPAddress _startingAddress;\n        readonly IPAddress _endingAddress;\n\n        #endregion\n\n        #region constructor\n\n        public Exclusion(IPAddress startingAddress, IPAddress endingAddress)\n        {\n            if (startingAddress.ConvertIpToNumber() > endingAddress.ConvertIpToNumber())\n                throw new ArgumentException(\"Exclusion ending address must be greater than or equal to starting address.\");\n\n            _startingAddress = startingAddress;\n            _endingAddress = endingAddress;\n        }\n\n        #endregion\n\n        #region properties\n\n        public IPAddress StartingAddress\n        { get { return _startingAddress; } }\n\n        public IPAddress EndingAddress\n        { get { return _endingAddress; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Lease.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dhcp.Options;\nusing System;\nusing System.Globalization;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerCore.Dhcp\n{\n    public enum LeaseType : byte\n    {\n        None = 0,\n        Dynamic = 1,\n        Reserved = 2\n    }\n\n    public class Lease : IComparable<Lease>\n    {\n        #region variables\n\n        static readonly char[] _hyphenColonSeparator = new char[] { '-', ':' };\n\n        LeaseType _type;\n        readonly ClientIdentifierOption _clientIdentifier;\n        string _hostName;\n        readonly byte[] _hardwareAddress;\n        readonly IPAddress _address;\n        string _comments;\n        readonly DateTime _leaseObtained;\n        DateTime _leaseExpires;\n\n        #endregion\n\n        #region constructor\n\n        internal Lease(LeaseType type, ClientIdentifierOption clientIdentifier, string hostName, byte[] hardwareAddress, IPAddress address, string comments, uint leaseTime)\n        {\n            _type = type;\n            _clientIdentifier = clientIdentifier;\n            _hostName = hostName;\n            _hardwareAddress = hardwareAddress;\n            _address = address;\n            _comments = comments;\n            _leaseObtained = DateTime.UtcNow;\n\n            ExtendLease(leaseTime);\n        }\n\n        internal Lease(LeaseType type, string hostName, DhcpMessageHardwareAddressType hardwareAddressType, byte[] hardwareAddress, IPAddress address, string comments)\n            : this(type, new ClientIdentifierOption((byte)hardwareAddressType, hardwareAddress), hostName, hardwareAddress, address, comments, 0)\n        { }\n\n        internal Lease(LeaseType type, string hostName, DhcpMessageHardwareAddressType hardwareAddressType, string hardwareAddress, IPAddress address, string comments)\n            : this(type, hostName, hardwareAddressType, ParseHardwareAddress(hardwareAddress), address, comments)\n        { }\n\n        internal Lease(BinaryReader bR)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                    _type = (LeaseType)bR.ReadByte();\n                    _clientIdentifier = DhcpOption.Parse(bR.BaseStream) as ClientIdentifierOption;\n                    _clientIdentifier.ParseOptionValue();\n\n                    _hostName = bR.ReadShortString();\n                    if (string.IsNullOrWhiteSpace(_hostName))\n                        _hostName = null;\n\n                    _hardwareAddress = bR.ReadBuffer();\n                    _address = IPAddressExtensions.ReadFrom(bR);\n\n                    if (version >= 2)\n                    {\n                        _comments = bR.ReadShortString();\n                        if (string.IsNullOrWhiteSpace(_comments))\n                            _comments = null;\n                    }\n\n                    _leaseObtained = bR.ReadDateTime();\n                    _leaseExpires = bR.ReadDateTime();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"Lease data format version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        internal static byte[] ParseHardwareAddress(string hardwareAddress)\n        {\n            string[] parts = hardwareAddress.Split(_hyphenColonSeparator);\n            byte[] address = new byte[parts.Length];\n\n            for (int i = 0; i < parts.Length; i++)\n                address[i] = byte.Parse(parts[i], NumberStyles.HexNumber, CultureInfo.InvariantCulture);\n\n            return address;\n        }\n\n        internal void ConvertToReserved()\n        {\n            _type = LeaseType.Reserved;\n        }\n\n        internal void ConvertToDynamic()\n        {\n            _type = LeaseType.Dynamic;\n        }\n\n        internal void SetHostName(string hostName)\n        {\n            _hostName = hostName;\n        }\n\n        #endregion\n\n        #region public\n\n        public void ExtendLease(uint leaseTime)\n        {\n            _leaseExpires = DateTime.UtcNow.AddSeconds(leaseTime);\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)2); //version\n\n            bW.Write((byte)_type);\n            _clientIdentifier.WriteTo(bW.BaseStream);\n\n            if (string.IsNullOrWhiteSpace(_hostName))\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_hostName);\n\n            bW.WriteBuffer(_hardwareAddress);\n            _address.WriteTo(bW);\n\n            if (string.IsNullOrWhiteSpace(_comments))\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_comments);\n\n            bW.Write(_leaseObtained);\n            bW.Write(_leaseExpires);\n        }\n\n        public string GetClientInfo()\n        {\n            string hardwareAddress = BitConverter.ToString(_hardwareAddress);\n\n            if (string.IsNullOrWhiteSpace(_hostName))\n                return \"[\" + hardwareAddress + \"]\";\n\n            return _hostName + \" [\" + hardwareAddress + \"]\";\n        }\n\n        public int CompareTo(Lease other)\n        {\n            return _address.ConvertIpToNumber().CompareTo(other._address.ConvertIpToNumber());\n        }\n\n        #endregion\n\n        #region properties\n\n        public LeaseType Type\n        { get { return _type; } }\n\n        internal ClientIdentifierOption ClientIdentifier\n        { get { return _clientIdentifier; } }\n\n        public string HostName\n        { get { return _hostName; } }\n\n        public byte[] HardwareAddress\n        { get { return _hardwareAddress; } }\n\n        public IPAddress Address\n        { get { return _address; } }\n\n        public string Comments\n        {\n            get { return _comments; }\n            set { _comments = value; }\n        }\n\n        public DateTime LeaseObtained\n        { get { return _leaseObtained; } }\n\n        public DateTime LeaseExpires\n        { get { return _leaseExpires; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/BroadcastAddressOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class BroadcastAddressOption : DhcpOption\n    {\n        #region variables\n\n        IPAddress _broadcastAddress;\n\n        #endregion\n\n        #region constructor\n\n        public BroadcastAddressOption(IPAddress broadcastAddress)\n            : base(DhcpOptionCode.BroadcastAddress)\n        {\n            _broadcastAddress = broadcastAddress;\n        }\n\n        public BroadcastAddressOption(Stream s)\n            : base(DhcpOptionCode.BroadcastAddress, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 4)\n                throw new InvalidDataException();\n\n            _broadcastAddress = new IPAddress(s.ReadExactly(4));\n\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(_broadcastAddress.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IPAddress BroadcastAddress\n        { get { return _broadcastAddress; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/CAPWAPAccessControllerOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class CAPWAPAccessControllerOption : DhcpOption\n    {\n        #region variables\n\n        IReadOnlyCollection<IPAddress> _apIpAddresses;\n\n        #endregion\n\n        #region constructor\n\n        public CAPWAPAccessControllerOption(IReadOnlyCollection<IPAddress> apIpAddresses)\n            : base(DhcpOptionCode.CAPWAPAccessControllerAddresses)\n        {\n            _apIpAddresses = apIpAddresses;\n        }\n\n        public CAPWAPAccessControllerOption(Stream s)\n            : base(DhcpOptionCode.CAPWAPAccessControllerAddresses, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 1)\n                throw new InvalidDataException();\n\n            List<IPAddress> apIpAddresses = new List<IPAddress>();\n\n            while (s.Length > 0)\n                apIpAddresses.Add(new IPAddress(s.ReadExactly(4)));\n\n            _apIpAddresses = apIpAddresses;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (IPAddress apIpAddress in _apIpAddresses)\n                s.Write(apIpAddress.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyCollection<IPAddress> ApIpAddresses\n        { get { return _apIpAddresses; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/ClasslessStaticRouteOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    public class ClasslessStaticRouteOption : DhcpOption\n    {\n        #region variables\n\n        IReadOnlyCollection<Route> _routes;\n\n        #endregion\n\n        #region constructor\n\n        public ClasslessStaticRouteOption(IReadOnlyCollection<Route> routes)\n            : base(DhcpOptionCode.ClasslessStaticRoute)\n        {\n            _routes = routes;\n        }\n\n        public ClasslessStaticRouteOption(Stream s)\n            : base(DhcpOptionCode.ClasslessStaticRoute, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 5)\n                throw new InvalidDataException();\n\n            List<Route> routes = new List<Route>();\n\n            while (s.Position < s.Length)\n            {\n                routes.Add(new Route(s));\n            }\n\n            _routes = routes;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (Route route in _routes)\n                route.WriteTo(s);\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyCollection<Route> Routes\n        { get { return _routes; } }\n\n        #endregion\n\n        public class Route\n        {\n            #region private\n\n            readonly IPAddress _destination;\n            readonly IPAddress _subnetMask;\n            readonly IPAddress _router;\n\n            #endregion\n\n            #region constructor\n\n            public Route(IPAddress destination, IPAddress subnetMask, IPAddress router)\n            {\n                _destination = destination;\n                _subnetMask = subnetMask;\n                _router = router;\n            }\n\n            public Route(Stream s)\n            {\n                int subnetMaskWidth = s.ReadByte();\n                if (subnetMaskWidth < 0)\n                    throw new EndOfStreamException();\n\n                byte[] destinationBuffer = new byte[4];\n                s.ReadExactly(destinationBuffer, 0, Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(subnetMaskWidth) / 8)));\n                _destination = new IPAddress(destinationBuffer);\n\n                _subnetMask = IPAddressExtensions.GetSubnetMask(subnetMaskWidth);\n\n                _router = new IPAddress(s.ReadExactly(4));\n            }\n\n            #endregion\n\n            #region public\n\n            public void WriteTo(Stream s)\n            {\n                byte subnetMaskWidth = (byte)_subnetMask.GetSubnetMaskWidth();\n\n                s.WriteByte(subnetMaskWidth);\n                s.Write(_destination.GetAddressBytes(), 0, Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(subnetMaskWidth) / 8)));\n                s.Write(_router.GetAddressBytes());\n            }\n\n            #endregion\n\n            #region properties\n\n            public IPAddress Destination\n            { get { return _destination; } }\n\n            public IPAddress SubnetMask\n            { get { return _subnetMask; } }\n\n            public IPAddress Router\n            { get { return _router; } }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/ClientFullyQualifiedDomainNameOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing System.Text;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    [Flags]\n    enum ClientFullyQualifiedDomainNameFlags : byte\n    {\n        None = 0,\n        ShouldUpdateDns = 1,\n        OverrideByServer = 2,\n        EncodeUsingCanonicalWireFormat = 4,\n        NoDnsUpdate = 8,\n    }\n\n    class ClientFullyQualifiedDomainNameOption : DhcpOption\n    {\n        #region variables\n\n        ClientFullyQualifiedDomainNameFlags _flags;\n        byte _rcode1;\n        byte _rcode2;\n        string _domainName;\n\n        #endregion\n\n        #region constructor\n\n        public ClientFullyQualifiedDomainNameOption(ClientFullyQualifiedDomainNameFlags flags, byte rcode1, byte rcode2, string domainName)\n            : base(DhcpOptionCode.ClientFullyQualifiedDomainName)\n        {\n            _flags = flags;\n            _rcode1 = rcode1;\n            _rcode2 = rcode2;\n            _domainName = domainName;\n        }\n\n        public ClientFullyQualifiedDomainNameOption(Stream s)\n            : base(DhcpOptionCode.ClientFullyQualifiedDomainName, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 3)\n                throw new InvalidDataException();\n\n            int flags = s.ReadByte();\n            if (flags < 0)\n                throw new EndOfStreamException();\n\n            _flags = (ClientFullyQualifiedDomainNameFlags)flags;\n\n            int rcode;\n\n            rcode = s.ReadByte();\n            if (rcode < 0)\n                throw new EndOfStreamException();\n\n            _rcode1 = (byte)rcode;\n\n            rcode = s.ReadByte();\n            if (rcode < 0)\n                throw new EndOfStreamException();\n\n            _rcode2 = (byte)rcode;\n\n            if (_flags.HasFlag(ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat))\n                _domainName = DnsDatagram.DeserializeDomainName(s, 0, true);\n            else\n                _domainName = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length - 3));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.WriteByte((byte)_flags);\n            s.WriteByte(_rcode1);\n            s.WriteByte(_rcode2);\n\n            if (_flags.HasFlag(ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat))\n                DnsDatagram.SerializeDomainName(_domainName, s);\n            else\n                s.Write(Encoding.ASCII.GetBytes(_domainName));\n        }\n\n        #endregion\n\n        #region properties\n\n        public ClientFullyQualifiedDomainNameFlags Flags\n        { get { return _flags; } }\n\n        public byte RCODE1\n        { get { return _rcode1; } }\n\n        public byte RCODE2\n        { get { return _rcode2; } }\n\n        public string DomainName\n        { get { return _domainName; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/ClientIdentifierOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    public class ClientIdentifierOption : DhcpOption, IEquatable<ClientIdentifierOption>\n    {\n        #region variables\n\n        byte _type;\n        byte[] _identifier;\n\n        #endregion\n\n        #region constructor\n\n        public ClientIdentifierOption(byte type, byte[] identifier)\n            : base(DhcpOptionCode.ClientIdentifier)\n        {\n            _type = type;\n            _identifier = identifier;\n        }\n\n        public ClientIdentifierOption(Stream s)\n            : base(DhcpOptionCode.ClientIdentifier, s)\n        { }\n\n        #endregion\n\n        #region static\n\n        public static ClientIdentifierOption Parse(string clientIdentifier)\n        {\n            string[] parts = clientIdentifier.Split('-');\n            return new ClientIdentifierOption(byte.Parse(parts[0]), Convert.FromHexString(parts[1]));\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 2)\n                throw new InvalidDataException();\n\n            int type = s.ReadByte();\n            if (type < 0)\n                throw new EndOfStreamException();\n\n            _type = (byte)type;\n            _identifier = s.ReadExactly((int)s.Length - 1);\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.WriteByte(_type);\n            s.Write(_identifier);\n        }\n\n        #endregion\n\n        #region public\n\n        public override bool Equals(object obj)\n        {\n            if (obj is null)\n                return false;\n\n            if (ReferenceEquals(this, obj))\n                return true;\n\n            return Equals(obj as ClientIdentifierOption);\n        }\n\n        public bool Equals(ClientIdentifierOption other)\n        {\n            if (other is null)\n                return false;\n\n            if (this._type != other._type)\n                return false;\n\n            if (this._identifier.Length != other._identifier.Length)\n                return false;\n\n            for (int i = 0; i < this._identifier.Length; i++)\n            {\n                if (this._identifier[i] != other._identifier[i])\n                    return false;\n            }\n\n            return true;\n        }\n\n        public override int GetHashCode()\n        {\n            return HashCode.Combine(_type, _identifier.GetArrayHashCode());\n        }\n\n        public override string ToString()\n        {\n            return _type.ToString() + \"-\" + Convert.ToHexString(_identifier);\n        }\n\n        #endregion\n\n        #region properties\n\n        public byte Type\n        { get { return _type; } }\n\n        public byte[] Identifier\n        { get { return _identifier; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/DhcpMessageTypeOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2019  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    enum DhcpMessageType : byte\n    {\n        Unknown = 0,\n        Discover = 1,\n        Offer = 2,\n        Request = 3,\n        Decline = 4,\n        Ack = 5,\n        Nak = 6,\n        Release = 7,\n        Inform = 8\n    }\n\n    class DhcpMessageTypeOption : DhcpOption\n    {\n        #region variables\n\n        DhcpMessageType _type;\n\n        #endregion\n\n        #region constructor\n\n        public DhcpMessageTypeOption(DhcpMessageType type)\n            : base(DhcpOptionCode.DhcpMessageType)\n        {\n            _type = type;\n        }\n\n        public DhcpMessageTypeOption(Stream s)\n            : base(DhcpOptionCode.DhcpMessageType, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 1)\n                throw new InvalidDataException();\n\n            int type = s.ReadByte();\n            if (type < 0)\n                throw new EndOfStreamException();\n\n            _type = (DhcpMessageType)type;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.WriteByte((byte)_type);\n        }\n\n        #endregion\n\n        #region string\n\n        public override string ToString()\n        {\n            return _type.ToString();\n        }\n\n        #endregion\n\n        #region properties\n\n        public DhcpMessageType Type\n        { get { return _type; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/DomainNameOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Text;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class DomainNameOption : DhcpOption\n    {\n        #region variables\n\n        string _domainName;\n\n        #endregion\n\n        #region constructor\n\n        public DomainNameOption(string domainName)\n            : base(DhcpOptionCode.DomainName)\n        {\n            _domainName = domainName;\n        }\n\n        public DomainNameOption(Stream s)\n            : base(DhcpOptionCode.DomainName, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 1)\n                throw new InvalidDataException();\n\n            _domainName = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(Encoding.ASCII.GetBytes(_domainName));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string DomainName\n        { get { return _domainName; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/DomainNameServerOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class DomainNameServerOption : DhcpOption\n    {\n        #region variables\n\n        IReadOnlyCollection<IPAddress> _addresses;\n\n        #endregion\n\n        #region constructor\n\n        public DomainNameServerOption(IReadOnlyCollection<IPAddress> addresses)\n            : base(DhcpOptionCode.DomainNameServer)\n        {\n            _addresses = addresses;\n        }\n\n        public DomainNameServerOption(Stream s)\n            : base(DhcpOptionCode.DomainNameServer, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if ((s.Length % 4 != 0) || (s.Length < 4))\n                throw new InvalidDataException();\n\n            IPAddress[] addresses = new IPAddress[s.Length / 4];\n\n            for (int i = 0; i < addresses.Length; i++)\n                addresses[i] = new IPAddress(s.ReadExactly(4));\n\n            _addresses = addresses;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (IPAddress address in _addresses)\n                s.Write(address.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyCollection<IPAddress> Addresses\n        { get { return _addresses; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/DomainSearchOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2022  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.IO;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class DomainSearchOption : DhcpOption\n    {\n        #region variables\n\n        IReadOnlyCollection<string> _searchStrings;\n\n        #endregion\n\n        #region constructor\n\n        public DomainSearchOption(IReadOnlyCollection<string> searchStrings)\n            : base(DhcpOptionCode.DomainSearch)\n        {\n            _searchStrings = searchStrings;\n        }\n\n        public DomainSearchOption(Stream s)\n            : base(DhcpOptionCode.DomainSearch, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 1)\n                throw new InvalidDataException();\n\n            List<string> searchStrings = new List<string>();\n\n            while (s.Length > 0)\n                searchStrings.Add(DnsDatagram.DeserializeDomainName(s));\n\n            _searchStrings = searchStrings;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            List<DnsDomainOffset> domainEntries = new List<DnsDomainOffset>(1);\n\n            foreach (string searchString in _searchStrings)\n                DnsDatagram.SerializeDomainName(searchString, s, domainEntries);\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyCollection<string> SearchStrings\n        { get { return _searchStrings; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/HostNameOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Text;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class HostNameOption : DhcpOption\n    {\n        #region variables\n\n        string _hostName;\n\n        #endregion\n\n        #region constructor\n\n        public HostNameOption(string hostName)\n            : base(DhcpOptionCode.HostName)\n        {\n            _hostName = hostName;\n        }\n\n        public HostNameOption(Stream s)\n            : base(DhcpOptionCode.HostName, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 1)\n                throw new InvalidDataException();\n\n            _hostName = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(Encoding.ASCII.GetBytes(_hostName));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string HostName\n        { get { return _hostName; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/IpAddressLeaseTimeOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class IpAddressLeaseTimeOption : DhcpOption\n    {\n        #region variables\n\n        uint _leaseTime;\n\n        #endregion\n\n        #region constructor\n\n        public IpAddressLeaseTimeOption(uint leaseTime)\n            : base(DhcpOptionCode.IpAddressLeaseTime)\n        {\n            _leaseTime = leaseTime;\n        }\n\n        public IpAddressLeaseTimeOption(Stream s)\n            : base(DhcpOptionCode.IpAddressLeaseTime, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 4)\n                throw new InvalidDataException();\n\n            byte[] buffer = s.ReadExactly(4);\n            Array.Reverse(buffer);\n            _leaseTime = BitConverter.ToUInt32(buffer, 0);\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            byte[] buffer = BitConverter.GetBytes(_leaseTime);\n            Array.Reverse(buffer);\n            s.Write(buffer);\n        }\n\n        #endregion\n\n        #region properties\n\n        public uint LeaseTime\n        { get { return _leaseTime; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/MaximumDhcpMessageSizeOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class MaximumDhcpMessageSizeOption : DhcpOption\n    {\n        #region variables\n\n        ushort _length;\n\n        #endregion\n\n        #region constructor\n\n        public MaximumDhcpMessageSizeOption(ushort length)\n            : base(DhcpOptionCode.MaximumDhcpMessageSize)\n        {\n            if (length < 576)\n                throw new ArgumentOutOfRangeException(nameof(length), \"Length must be 576 bytes or more.\");\n\n            _length = length;\n        }\n\n        public MaximumDhcpMessageSizeOption(Stream s)\n            : base(DhcpOptionCode.MaximumDhcpMessageSize, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 2)\n                throw new InvalidDataException();\n\n            byte[] buffer = s.ReadExactly(2);\n            Array.Reverse(buffer);\n            _length = BitConverter.ToUInt16(buffer, 0);\n\n            if (_length < 576)\n                _length = 576;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            byte[] buffer = BitConverter.GetBytes(_length);\n            Array.Reverse(buffer);\n            s.Write(buffer);\n        }\n\n        #endregion\n\n        #region properties\n\n        public uint Length\n        { get { return _length; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/NetBiosNameServerOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class NetBiosNameServerOption : DhcpOption\n    {\n        #region variables\n\n        IReadOnlyCollection<IPAddress> _addresses;\n\n        #endregion\n\n        #region constructor\n\n        public NetBiosNameServerOption(IReadOnlyCollection<IPAddress> addresses)\n            : base(DhcpOptionCode.NetBiosOverTcpIpNameServer)\n        {\n            _addresses = addresses;\n        }\n\n        public NetBiosNameServerOption(Stream s)\n            : base(DhcpOptionCode.NetBiosOverTcpIpNameServer, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if ((s.Length % 4 != 0) || (s.Length < 4))\n                throw new InvalidDataException();\n\n            IPAddress[] addresses = new IPAddress[s.Length / 4];\n\n            for (int i = 0; i < addresses.Length; i++)\n                addresses[i] = new IPAddress(s.ReadExactly(4));\n\n            _addresses = addresses;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (IPAddress address in _addresses)\n                s.Write(address.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyCollection<IPAddress> Addresses\n        { get { return _addresses; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/NetworkTimeProtocolServersOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class NetworkTimeProtocolServersOption : DhcpOption\n    {\n        #region variables\n\n        IReadOnlyCollection<IPAddress> _addresses;\n\n        #endregion\n\n        #region constructor\n\n        public NetworkTimeProtocolServersOption(IReadOnlyCollection<IPAddress> addresses)\n            : base(DhcpOptionCode.NetworkTimeProtocolServers)\n        {\n            _addresses = addresses;\n        }\n\n        public NetworkTimeProtocolServersOption(Stream s)\n            : base(DhcpOptionCode.NetworkTimeProtocolServers, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if ((s.Length % 4 != 0) || (s.Length < 4))\n                throw new InvalidDataException();\n\n            IPAddress[] addresses = new IPAddress[s.Length / 4];\n\n            for (int i = 0; i < addresses.Length; i++)\n                addresses[i] = new IPAddress(s.ReadExactly(4));\n\n            _addresses = addresses;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (IPAddress address in _addresses)\n                s.Write(address.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyCollection<IPAddress> Addresses\n        { get { return _addresses; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/OptionOverloadOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    [Flags]\n    enum OptionOverloadValue : byte\n    {\n        FileFieldUsed = 1,\n        SnameFieldUsed = 2,\n        BothFieldsUsed = 3\n    }\n\n    class OptionOverloadOption : DhcpOption\n    {\n        #region variables\n\n        OptionOverloadValue _value;\n\n        #endregion\n\n        #region constructor\n\n        public OptionOverloadOption(OptionOverloadValue value)\n            : base(DhcpOptionCode.OptionOverload)\n        {\n            _value = value;\n        }\n\n        public OptionOverloadOption(Stream s)\n            : base(DhcpOptionCode.OptionOverload, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 1)\n                throw new InvalidDataException();\n\n            int value = s.ReadByte();\n            if (value < 0)\n                throw new EndOfStreamException();\n\n            _value = (OptionOverloadValue)value;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.WriteByte((byte)_value);\n        }\n\n        #endregion\n\n        #region properties\n\n        public OptionOverloadValue Value\n        { get { return _value; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/ParameterRequestListOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2019  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class ParameterRequestListOption : DhcpOption\n    {\n        #region variables\n\n        DhcpOptionCode[] _optionCodes;\n\n        #endregion\n\n        #region constructor\n\n        public ParameterRequestListOption(DhcpOptionCode[] optionCodes)\n            : base(DhcpOptionCode.ParameterRequestList)\n        {\n            _optionCodes = optionCodes;\n        }\n\n        public ParameterRequestListOption(Stream s)\n            : base(DhcpOptionCode.ParameterRequestList, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length < 1)\n                throw new InvalidDataException();\n\n            _optionCodes = new DhcpOptionCode[s.Length];\n            int optionCode;\n\n            for (int i = 0; i < _optionCodes.Length; i++)\n            {\n                optionCode = s.ReadByte();\n                if (optionCode < 0)\n                    throw new EndOfStreamException();\n\n                _optionCodes[i] = (DhcpOptionCode)optionCode;\n            }\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (DhcpOptionCode optionCode in _optionCodes)\n                s.WriteByte((byte)optionCode);\n        }\n\n        #endregion\n\n        #region properties\n\n        public DhcpOptionCode[] OptionCodes\n        { get { return _optionCodes; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/RebindingTimeValueOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class RebindingTimeValueOption : DhcpOption\n    {\n        #region variables\n\n        uint _t2Interval;\n\n        #endregion\n\n        #region constructor\n\n        public RebindingTimeValueOption(uint t2Interval)\n            : base(DhcpOptionCode.RebindingTimeValue)\n        {\n            _t2Interval = t2Interval;\n        }\n\n        public RebindingTimeValueOption(Stream s)\n            : base(DhcpOptionCode.RebindingTimeValue, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 4)\n                throw new InvalidDataException();\n\n            byte[] buffer = s.ReadExactly(4);\n            Array.Reverse(buffer);\n            _t2Interval = BitConverter.ToUInt32(buffer, 0);\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            byte[] buffer = BitConverter.GetBytes(_t2Interval);\n            Array.Reverse(buffer);\n            s.Write(buffer);\n        }\n\n        #endregion\n\n        #region properties\n\n        public uint T2Interval\n        { get { return _t2Interval; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/RenewalTimeValueOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class RenewalTimeValueOption : DhcpOption\n    {\n        #region variables\n\n        uint _t1Interval;\n\n        #endregion\n\n        #region constructor\n\n        public RenewalTimeValueOption(uint t1Interval)\n            : base(DhcpOptionCode.RenewalTimeValue)\n        {\n            _t1Interval = t1Interval;\n        }\n\n        public RenewalTimeValueOption(Stream s)\n            : base(DhcpOptionCode.RenewalTimeValue, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 4)\n                throw new InvalidDataException();\n\n            byte[] buffer = s.ReadExactly(4);\n            Array.Reverse(buffer);\n            _t1Interval = BitConverter.ToUInt32(buffer, 0);\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            byte[] buffer = BitConverter.GetBytes(_t1Interval);\n            Array.Reverse(buffer);\n            s.Write(buffer);\n        }\n\n        #endregion\n\n        #region properties\n\n        public uint T1Interval\n        { get { return _t1Interval; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/RequestedIpAddressOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class RequestedIpAddressOption : DhcpOption\n    {\n        #region variables\n\n        IPAddress _address;\n\n        #endregion\n\n        #region constructor\n\n        public RequestedIpAddressOption(IPAddress address)\n            : base(DhcpOptionCode.RequestedIpAddress)\n        {\n            _address = address;\n        }\n\n        public RequestedIpAddressOption(Stream s)\n            : base(DhcpOptionCode.RequestedIpAddress, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 4)\n                throw new InvalidDataException();\n\n            _address = new IPAddress(s.ReadExactly(4));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(_address.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IPAddress Address\n        { get { return _address; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/RouterOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class RouterOption : DhcpOption\n    {\n        #region variables\n\n        IPAddress[] _addresses;\n\n        #endregion\n\n        #region constructor\n\n        public RouterOption(IPAddress[] addresses)\n            : base(DhcpOptionCode.Router)\n        {\n            _addresses = addresses;\n        }\n\n        public RouterOption(Stream s)\n            : base(DhcpOptionCode.Router, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if ((s.Length % 4 != 0) || (s.Length < 4))\n                throw new InvalidDataException();\n\n            _addresses = new IPAddress[s.Length / 4];\n\n            for (int i = 0; i < _addresses.Length; i++)\n                _addresses[i] = new IPAddress(s.ReadExactly(4));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (IPAddress address in _addresses)\n                s.Write(address.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IPAddress[] Addresses\n        { get { return _addresses; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/ServerIdentifierOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class ServerIdentifierOption : DhcpOption\n    {\n        #region variables\n\n        IPAddress _address;\n\n        #endregion\n\n        #region constructor\n\n        public ServerIdentifierOption(IPAddress address)\n            : base(DhcpOptionCode.ServerIdentifier)\n        {\n            _address = address;\n        }\n\n        public ServerIdentifierOption(Stream s)\n            : base(DhcpOptionCode.ServerIdentifier, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 4)\n                throw new InvalidDataException();\n\n            _address = new IPAddress(s.ReadExactly(4));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(_address.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IPAddress Address\n        { get { return _address; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/SubnetMaskOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class SubnetMaskOption : DhcpOption\n    {\n        #region variables\n\n        IPAddress _subnetMask;\n\n        #endregion\n\n        #region constructor\n\n        public SubnetMaskOption(IPAddress subnetMask)\n            : base(DhcpOptionCode.SubnetMask)\n        {\n            _subnetMask = subnetMask;\n        }\n\n        public SubnetMaskOption(Stream s)\n            : base(DhcpOptionCode.SubnetMask, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if (s.Length != 4)\n                throw new InvalidDataException();\n\n            _subnetMask = new IPAddress(s.ReadExactly(4));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(_subnetMask.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IPAddress SubnetMask\n        { get { return _subnetMask; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/TftpServerAddressOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class TftpServerAddressOption : DhcpOption\n    {\n        #region variables\n\n        IReadOnlyCollection<IPAddress> _addresses;\n\n        #endregion\n\n        #region constructor\n\n        public TftpServerAddressOption(IReadOnlyCollection<IPAddress> addresses)\n            : base(DhcpOptionCode.TftpServerAddress)\n        {\n            _addresses = addresses;\n        }\n\n        public TftpServerAddressOption(Stream s)\n            : base(DhcpOptionCode.TftpServerAddress, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            if ((s.Length % 4 != 0) || (s.Length < 4))\n                throw new InvalidDataException();\n\n            IPAddress[] addresses = new IPAddress[s.Length / 4];\n\n            for (int i = 0; i < addresses.Length; i++)\n                addresses[i] = new IPAddress(s.ReadExactly(4));\n\n            _addresses = addresses;\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            foreach (IPAddress address in _addresses)\n                s.Write(address.GetAddressBytes());\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyCollection<IPAddress> Addresses\n        { get { return _addresses; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/VendorClassIdentifierOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Text;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    class VendorClassIdentifierOption : DhcpOption\n    {\n        #region variables\n\n        string _identifier;\n\n        #endregion\n\n        #region constructor\n\n        public VendorClassIdentifierOption(string identifier)\n            : base(DhcpOptionCode.VendorClassIdentifier)\n        {\n            _identifier = identifier;\n        }\n\n        public VendorClassIdentifierOption(Stream s)\n            : base(DhcpOptionCode.VendorClassIdentifier, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            _identifier = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length));\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(Encoding.ASCII.GetBytes(_identifier));\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Identifier\n        { get { return _identifier; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Options/VendorSpecificInformationOption.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dhcp.Options\n{\n    public class VendorSpecificInformationOption : DhcpOption\n    {\n        #region variables\n\n        byte[] _information;\n\n        #endregion\n\n        #region constructor\n\n        public VendorSpecificInformationOption(string hexInfo)\n            : base(DhcpOptionCode.VendorSpecificInformation)\n        {\n            if (hexInfo.Contains(':'))\n                _information = hexInfo.ParseColonHexString();\n            else\n                _information = Convert.FromHexString(hexInfo);\n        }\n\n        public VendorSpecificInformationOption(byte[] information)\n            : base(DhcpOptionCode.VendorSpecificInformation)\n        {\n            _information = information;\n        }\n\n        public VendorSpecificInformationOption(Stream s)\n            : base(DhcpOptionCode.VendorSpecificInformation, s)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ParseOptionValue(Stream s)\n        {\n            _information = s.ReadExactly((int)s.Length);\n        }\n\n        protected override void WriteOptionValue(Stream s)\n        {\n            s.Write(_information);\n        }\n\n        #endregion\n\n        #region properties\n\n        public byte[] Information\n        { get { return _information; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dhcp/Scope.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dhcp.Options;\nusing DnsServerCore.Dns;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dhcp\n{\n    public sealed class Scope : IComparable<Scope>, IDisposable\n    {\n        #region variables\n\n        //required parameters\n        string _name;\n        bool _enabled;\n        IPAddress _startingAddress;\n        IPAddress _endingAddress;\n        IPAddress _subnetMask;\n        ushort _leaseTimeDays = 1; //default 1 day lease\n        byte _leaseTimeHours = 0;\n        byte _leaseTimeMinutes = 0;\n        ushort _offerDelayTime;\n\n        readonly LogManager _log;\n        readonly DhcpServer _dhcpServer;\n\n        bool _pingCheckEnabled;\n        ushort _pingCheckTimeout = 1000;\n        byte _pingCheckRetries = 2;\n\n        //dhcp options\n        string _domainName;\n        IReadOnlyCollection<string> _domainSearchList;\n        bool _dnsUpdates = true;\n        bool _dnsOverwriteForDynamicLease = false;\n        uint _dnsTtl = 900;\n        IPAddress _serverAddress;\n        string _serverHostName;\n        string _bootFileName;\n        IPAddress _routerAddress;\n        bool _useThisDnsServer;\n        IReadOnlyCollection<IPAddress> _dnsServers;\n        IReadOnlyCollection<IPAddress> _winsServers;\n        IReadOnlyCollection<IPAddress> _ntpServers;\n        IReadOnlyCollection<string> _ntpServerDomainNames;\n        IReadOnlyCollection<ClasslessStaticRouteOption.Route> _staticRoutes;\n        IReadOnlyDictionary<string, VendorSpecificInformationOption> _vendorInfo;\n        IReadOnlyCollection<IPAddress> _capwapAcIpAddresses;\n        IReadOnlyCollection<IPAddress> _tftpServerAddreses;\n\n        //advanced options\n        IReadOnlyCollection<DhcpOption> _genericOptions;\n        IReadOnlyCollection<Exclusion> _exclusions;\n        readonly ConcurrentDictionary<ClientIdentifierOption, Lease> _reservedLeases = new ConcurrentDictionary<ClientIdentifierOption, Lease>();\n        bool _allowOnlyReservedLeases;\n        bool _blockLocallyAdministeredMacAddresses;\n        bool _ignoreClientIdentifierOption;\n\n        //leases\n        readonly ConcurrentDictionary<ClientIdentifierOption, Lease> _leases = new ConcurrentDictionary<ClientIdentifierOption, Lease>();\n\n        //internal computed parameters\n        IPAddress _networkAddress;\n        IPAddress _broadcastAddress;\n\n        //internal parameters\n        const int OFFER_EXPIRY_SECONDS = 60; //1 mins offer expiry\n        readonly ConcurrentDictionary<ClientIdentifierOption, Lease> _offers = new ConcurrentDictionary<ClientIdentifierOption, Lease>();\n        IPAddress _lastAddressOffered;\n        readonly SemaphoreSlim _lastAddressOfferedLock = new SemaphoreSlim(1, 1);\n        IPAddress _interfaceAddress;\n        int _interfaceIndex;\n        DateTime _lastModified = DateTime.UtcNow;\n\n        #endregion\n\n        #region constructor\n\n        public Scope(string name, bool enabled, IPAddress startingAddress, IPAddress endingAddress, IPAddress subnetMask, LogManager log, DhcpServer dhcpServer)\n        {\n            ValidateScopeName(name);\n\n            _name = name;\n            _enabled = enabled;\n\n            ChangeNetwork(startingAddress, endingAddress, subnetMask);\n\n            _log = log;\n            _dhcpServer = dhcpServer;\n        }\n\n        public Scope(Stream s, LogManager log, DhcpServer dhcpServer)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"SC\")\n                throw new InvalidDataException(\"DhcpServer scope file format is invalid.\");\n\n            _log = log;\n            _dhcpServer = dhcpServer;\n\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                case 3:\n                case 4:\n                case 5:\n                case 6:\n                case 7:\n                case 8:\n                case 9:\n                case 10:\n                    _name = bR.ReadShortString();\n                    _enabled = bR.ReadBoolean();\n\n                    ChangeNetwork(IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR));\n\n                    _leaseTimeDays = bR.ReadUInt16();\n                    _leaseTimeHours = bR.ReadByte();\n                    _leaseTimeMinutes = bR.ReadByte();\n\n                    _offerDelayTime = bR.ReadUInt16();\n\n                    if (version >= 5)\n                    {\n                        _pingCheckEnabled = bR.ReadBoolean();\n                        _pingCheckTimeout = bR.ReadUInt16();\n                        _pingCheckRetries = bR.ReadByte();\n                    }\n\n                    _domainName = bR.ReadShortString();\n                    if (string.IsNullOrWhiteSpace(_domainName))\n                        _domainName = null;\n\n                    if (version >= 7)\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            string[] domainSearchStrings = new string[count];\n\n                            for (int i = 0; i < count; i++)\n                                domainSearchStrings[i] = bR.ReadShortString();\n\n                            _domainSearchList = domainSearchStrings;\n                        }\n\n                        _dnsUpdates = bR.ReadBoolean();\n                    }\n\n                    if (version >= 10)\n                        _dnsOverwriteForDynamicLease = bR.ReadBoolean();\n\n                    _dnsTtl = bR.ReadUInt32();\n\n                    if (version >= 2)\n                    {\n                        _serverAddress = IPAddressExtensions.ReadFrom(bR);\n                        if (_serverAddress.Equals(IPAddress.Any))\n                            _serverAddress = null;\n                    }\n\n                    if (version >= 3)\n                    {\n                        _serverHostName = bR.ReadShortString();\n                        if (string.IsNullOrEmpty(_serverHostName))\n                            _serverHostName = null;\n\n                        _bootFileName = bR.ReadShortString();\n                        if (string.IsNullOrEmpty(_bootFileName))\n                            _bootFileName = null;\n                    }\n\n                    _routerAddress = IPAddressExtensions.ReadFrom(bR);\n                    if (_routerAddress.Equals(IPAddress.Any))\n                        _routerAddress = null;\n\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            if (count == 255)\n                            {\n                                _useThisDnsServer = true;\n                                FindThisDnsServerAddress();\n                            }\n                            else\n                            {\n                                IPAddress[] dnsServers = new IPAddress[count];\n\n                                for (int i = 0; i < count; i++)\n                                    dnsServers[i] = IPAddressExtensions.ReadFrom(bR);\n\n                                _dnsServers = dnsServers;\n                            }\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            IPAddress[] winsServers = new IPAddress[count];\n\n                            for (int i = 0; i < count; i++)\n                                winsServers[i] = IPAddressExtensions.ReadFrom(bR);\n\n                            _winsServers = winsServers;\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            IPAddress[] ntpServers = new IPAddress[count];\n\n                            for (int i = 0; i < count; i++)\n                                ntpServers[i] = IPAddressExtensions.ReadFrom(bR);\n\n                            _ntpServers = ntpServers;\n                        }\n                    }\n\n                    if (version >= 7)\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            string[] ntpServerDomainNames = new string[count];\n\n                            for (int i = 0; i < count; i++)\n                                ntpServerDomainNames[i] = bR.ReadShortString();\n\n                            _ntpServerDomainNames = ntpServerDomainNames;\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            ClasslessStaticRouteOption.Route[] staticRoutes = new ClasslessStaticRouteOption.Route[count];\n\n                            for (int i = 0; i < count; i++)\n                                staticRoutes[i] = new ClasslessStaticRouteOption.Route(bR.BaseStream);\n\n                            _staticRoutes = staticRoutes;\n                        }\n                    }\n\n                    if (version >= 4)\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            Dictionary<string, VendorSpecificInformationOption> vendorInfo = new Dictionary<string, VendorSpecificInformationOption>(count);\n\n                            for (int i = 0; i < count; i++)\n                            {\n                                string vendorClassIdentifier = bR.ReadShortString();\n                                VendorSpecificInformationOption vendorSpecificInformation = new VendorSpecificInformationOption(bR.ReadBuffer());\n\n                                vendorInfo.Add(vendorClassIdentifier, vendorSpecificInformation);\n                            }\n\n                            _vendorInfo = vendorInfo;\n                        }\n                    }\n\n                    if (version >= 7)\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            IPAddress[] capwapAcIpAddresses = new IPAddress[count];\n\n                            for (int i = 0; i < count; i++)\n                                capwapAcIpAddresses[i] = IPAddressExtensions.ReadFrom(bR);\n\n                            _capwapAcIpAddresses = capwapAcIpAddresses;\n                        }\n                    }\n\n                    if (version >= 8)\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            IPAddress[] tftpServerAddreses = new IPAddress[count];\n\n                            for (int i = 0; i < count; i++)\n                                tftpServerAddreses[i] = IPAddressExtensions.ReadFrom(bR);\n\n                            _tftpServerAddreses = tftpServerAddreses;\n                        }\n                    }\n\n                    if (version >= 8)\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            DhcpOption[] genericOptions = new DhcpOption[count];\n\n                            for (int i = 0; i < count; i++)\n                            {\n                                DhcpOptionCode code = (DhcpOptionCode)bR.ReadByte();\n                                short length = bR.ReadInt16();\n                                byte[] value = bR.ReadBytes(length);\n\n                                genericOptions[i] = new DhcpOption(code, value);\n                            }\n\n                            _genericOptions = genericOptions;\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadByte();\n                        if (count > 0)\n                        {\n                            Exclusion[] exclusions = new Exclusion[count];\n\n                            for (int i = 0; i < count; i++)\n                                exclusions[i] = new Exclusion(IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR));\n\n                            _exclusions = exclusions;\n                        }\n                    }\n\n                    {\n                        int count = bR.ReadInt32();\n                        if (count > 0)\n                        {\n                            for (int i = 0; i < count; i++)\n                            {\n                                Lease reservedLease = new Lease(bR);\n                                _reservedLeases.TryAdd(reservedLease.ClientIdentifier, reservedLease);\n                            }\n                        }\n\n                        _allowOnlyReservedLeases = bR.ReadBoolean();\n                    }\n\n                    if (version >= 6)\n                        _blockLocallyAdministeredMacAddresses = bR.ReadBoolean();\n                    else\n                        _blockLocallyAdministeredMacAddresses = false;\n\n                    if (version >= 9)\n                        _ignoreClientIdentifierOption = bR.ReadBoolean();\n                    else\n                        _ignoreClientIdentifierOption = false;\n\n                    {\n                        int count = bR.ReadInt32();\n                        if (count > 0)\n                        {\n                            for (int i = 0; i < count; i++)\n                            {\n                                Lease lease = new Lease(bR);\n\n                                _leases.TryAdd(lease.ClientIdentifier, lease);\n                            }\n                        }\n                    }\n\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"Scope data format version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _lastAddressOfferedLock?.Dispose();\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region static\n\n        internal static void ValidateScopeName(string name)\n        {\n            foreach (char invalidChar in Path.GetInvalidFileNameChars())\n            {\n                if (name.Contains(invalidChar))\n                    throw new DhcpServerException(\"The scope name contains an invalid character: \" + invalidChar);\n            }\n        }\n\n        private static bool IsAddressInRange(IPAddress address, IPAddress startingAddress, IPAddress endingAddress)\n        {\n            uint addressNumber = address.ConvertIpToNumber();\n            uint startingAddressNumber = startingAddress.ConvertIpToNumber();\n            uint endingAddressNumber = endingAddress.ConvertIpToNumber();\n\n            return (startingAddressNumber <= addressNumber) && (addressNumber <= endingAddressNumber);\n        }\n\n        private static void ValidateIpv4(IReadOnlyCollection<IPAddress> value, string paramName)\n        {\n            if (value is not null)\n            {\n                foreach (IPAddress ip in value)\n                {\n                    if (ip.AddressFamily != AddressFamily.InterNetwork)\n                        throw new ArgumentException(\"The address must be an IPv4 address: \" + ip.ToString(), paramName);\n                }\n            }\n        }\n\n        private static void ValidateIpv4(IPAddress value, string paramName)\n        {\n            if ((value is not null) && (value.AddressFamily != AddressFamily.InterNetwork))\n                throw new ArgumentException(\"The address must be an IPv4 address: \" + value.ToString(), paramName);\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task<AddressStatus> IsAddressAvailableAsync(IPAddress address)\n        {\n            if (address.Equals(_routerAddress))\n                return AddressStatus.FALSE;\n\n            if ((_dnsServers != null) && _dnsServers.Contains(address))\n                return AddressStatus.FALSE;\n\n            if ((_winsServers != null) && _winsServers.Contains(address))\n                return AddressStatus.FALSE;\n\n            if ((_ntpServers != null) && _ntpServers.Contains(address))\n                return AddressStatus.FALSE;\n\n            if (_exclusions != null)\n            {\n                foreach (Exclusion exclusion in _exclusions)\n                {\n                    if (IsAddressInRange(address, exclusion.StartingAddress, exclusion.EndingAddress))\n                        return new AddressStatus(false, exclusion.EndingAddress);\n                }\n            }\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> reservedLease in _reservedLeases)\n            {\n                if (address.Equals(reservedLease.Value.Address))\n                    return AddressStatus.FALSE;\n            }\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in _leases)\n            {\n                if (address.Equals(lease.Value.Address))\n                    return AddressStatus.FALSE;\n            }\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> offer in _offers)\n            {\n                if (address.Equals(offer.Value.Address))\n                    return AddressStatus.FALSE;\n            }\n\n            if (_pingCheckEnabled)\n            {\n                try\n                {\n                    using (Ping ping = new Ping())\n                    {\n                        int retry = 0;\n                        do\n                        {\n                            PingReply reply = await ping.SendPingAsync(address, _pingCheckTimeout);\n                            if (reply.Status == IPStatus.Success)\n                                return AddressStatus.FALSE; //address is in use\n                        }\n                        while (++retry < _pingCheckRetries);\n                    }\n                }\n                catch\n                { }\n            }\n\n            return AddressStatus.TRUE;\n        }\n\n        private bool IsAddressAlreadyAllocated(IPAddress address, ClientIdentifierOption clientIdentifier)\n        {\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in _leases)\n            {\n                if (address.Equals(lease.Value.Address))\n                    return !lease.Key.Equals(clientIdentifier);\n            }\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> offer in _offers)\n            {\n                if (address.Equals(offer.Value.Address))\n                    return !offer.Key.Equals(clientIdentifier);\n            }\n\n            return false;\n        }\n\n        private ClientFullyQualifiedDomainNameOption GetClientFullyQualifiedDomainNameOption(DhcpMessage request, string reservedLeaseHostName)\n        {\n            ClientFullyQualifiedDomainNameFlags responseFlags = ClientFullyQualifiedDomainNameFlags.None;\n\n            if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat))\n                responseFlags |= ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat;\n\n            if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.NoDnsUpdate))\n            {\n                responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns;\n                responseFlags |= ClientFullyQualifiedDomainNameFlags.OverrideByServer;\n            }\n            else if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns))\n            {\n                responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns;\n            }\n            else\n            {\n                responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns;\n                responseFlags |= ClientFullyQualifiedDomainNameFlags.OverrideByServer;\n            }\n\n            string clientDomainName;\n\n            if (!string.IsNullOrWhiteSpace(reservedLeaseHostName))\n            {\n                //domain name override by server\n                clientDomainName = DhcpServer.GetSanitizedHostName(reservedLeaseHostName) + \".\" + _domainName;\n            }\n            else if (string.IsNullOrWhiteSpace(request.ClientFullyQualifiedDomainName.DomainName))\n            {\n                //client domain empty and expects server for a fqdn domain name\n                if (request.HostName is null)\n                    return null; //server unable to decide a name for client\n\n                clientDomainName = DhcpServer.GetSanitizedHostName(request.HostName.HostName) + \".\" + _domainName;\n            }\n            else if (request.ClientFullyQualifiedDomainName.DomainName.Contains('.'))\n            {\n                //client domain is fqdn\n                if (request.ClientFullyQualifiedDomainName.DomainName.EndsWith(\".\" + _domainName, StringComparison.OrdinalIgnoreCase))\n                {\n                    clientDomainName = request.ClientFullyQualifiedDomainName.DomainName;\n                }\n                else\n                {\n                    string[] parts = request.ClientFullyQualifiedDomainName.DomainName.Split('.');\n                    clientDomainName = parts[0] + \".\" + _domainName;\n                }\n            }\n            else\n            {\n                //client domain is just hostname\n                clientDomainName = request.ClientFullyQualifiedDomainName.DomainName + \".\" + _domainName;\n            }\n\n            return new ClientFullyQualifiedDomainNameOption(responseFlags, 255, 255, clientDomainName);\n        }\n\n        private void ConvertToReservedLease(Lease lease)\n        {\n            //convert dynamic to reserved lease\n            lease.ConvertToReserved();\n\n            //add reserved lease\n            Lease reservedLease = new Lease(LeaseType.Reserved, null, DhcpMessageHardwareAddressType.Ethernet, lease.HardwareAddress, lease.Address, null);\n            _reservedLeases[reservedLease.ClientIdentifier] = reservedLease;\n        }\n\n        private void ConvertToDynamicLease(Lease lease)\n        {\n            //convert reserved to dynamic lease\n            lease.ConvertToDynamic();\n\n            //remove reserved lease\n            Lease reservedLease = new Lease(LeaseType.Reserved, null, DhcpMessageHardwareAddressType.Ethernet, lease.HardwareAddress, lease.Address, null);\n            _reservedLeases.TryRemove(reservedLease.ClientIdentifier, out _);\n\n            //remove any old single address exclusion entry\n            if (_exclusions != null)\n            {\n                foreach (Exclusion exclusion in _exclusions)\n                {\n                    if (exclusion.StartingAddress.Equals(lease.Address) && exclusion.EndingAddress.Equals(lease.Address))\n                    {\n                        //remove single address exclusion entry\n                        if (_exclusions.Count == 1)\n                        {\n                            _exclusions = null;\n                        }\n                        else\n                        {\n                            List<Exclusion> exclusions = new List<Exclusion>();\n\n                            foreach (Exclusion exc in _exclusions)\n                            {\n                                if (exc.Equals(exclusion))\n                                    continue;\n\n                                exclusions.Add(exc);\n                            }\n\n                            _exclusions = exclusions;\n                        }\n\n                        break;\n                    }\n                }\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        internal bool FindInterface()\n        {\n            //find network with static ip address in scope range\n            uint networkAddressNumber = _networkAddress.ConvertIpToNumber();\n            uint subnetMaskNumber = _subnetMask.ConvertIpToNumber();\n\n            foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())\n            {\n                if (nic.OperationalStatus != OperationalStatus.Up)\n                    continue;\n\n                IPInterfaceProperties ipInterface = nic.GetIPProperties();\n\n                foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses)\n                {\n                    if (ip.Address.AddressFamily == AddressFamily.InterNetwork)\n                    {\n                        uint addressNumber = ip.Address.ConvertIpToNumber();\n\n                        if ((addressNumber & subnetMaskNumber) == networkAddressNumber)\n                        {\n                            //found interface for this scope range\n\n                            try\n                            {\n                                //check if interface has dynamic ipv4 address assigned via dhcp\n                                if (!OperatingSystem.IsMacOS())\n                                {\n                                    foreach (IPAddress dhcpServerAddress in ipInterface.DhcpServerAddresses)\n                                    {\n                                        if (dhcpServerAddress.AddressFamily == AddressFamily.InterNetwork)\n                                            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());\n                                    }\n                                }\n                            }\n                            catch (PlatformNotSupportedException)\n                            {\n                                //DhcpServerAddresses() not supported on macOs\n                                //ignore the exception\n                            }\n\n                            _interfaceAddress = ip.Address;\n                            _interfaceIndex = ipInterface.GetIPv4Properties().Index;\n                            return true;\n                        }\n                    }\n                }\n            }\n\n            try\n            {\n                if (!OperatingSystem.IsMacOS())\n                {\n                    //check if at least one interface has static ip address\n                    foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())\n                    {\n                        if (nic.OperationalStatus != OperationalStatus.Up)\n                            continue;\n\n                        IPInterfaceProperties ipInterface = nic.GetIPProperties();\n\n                        foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses)\n                        {\n                            if (ip.Address.AddressFamily == AddressFamily.InterNetwork)\n                            {\n                                //check if address is static\n                                if (ipInterface.DhcpServerAddresses.Count < 1)\n                                {\n                                    //found static ip address so this scope can be activated\n                                    //using ANY ip address for this scope interface since we dont know the relay agent network \n                                    _interfaceAddress = IPAddress.Any;\n                                    _interfaceIndex = -1;\n                                    return true;\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            catch (PlatformNotSupportedException)\n            {\n                //DhcpServerAddresses() not supported on macOs\n                //ignore the exception\n            }\n\n            //server has no static ip address configured\n            return false;\n        }\n\n        internal void FindThisDnsServerAddress()\n        {\n            uint networkAddressNumber = _networkAddress.ConvertIpToNumber();\n            uint subnetMaskNumber = _subnetMask.ConvertIpToNumber();\n\n            DnsServer dnsServer = _dhcpServer.DnsServer;\n            if (dnsServer is not null)\n            {\n                bool dnsOnAny = false;\n\n                foreach (IPEndPoint localEP in dnsServer.LocalEndPoints)\n                {\n                    if (localEP.Address.Equals(IPAddress.Any))\n                    {\n                        dnsOnAny = true;\n                        break;\n                    }\n                }\n\n                if (!dnsOnAny)\n                {\n                    //find local EP in scope network range\n                    foreach (IPEndPoint localEP in dnsServer.LocalEndPoints)\n                    {\n                        if (localEP.Address.AddressFamily == AddressFamily.InterNetwork)\n                        {\n                            uint addressNumber = localEP.Address.ConvertIpToNumber();\n\n                            if ((addressNumber & subnetMaskNumber) == networkAddressNumber)\n                            {\n                                //found address in this scope range to use as dns server\n                                _dnsServers = new IPAddress[] { localEP.Address };\n                                return;\n                            }\n                        }\n                    }\n\n                    //find any local EP available\n                    foreach (IPEndPoint localEP in dnsServer.LocalEndPoints)\n                    {\n                        if ((localEP.Address.AddressFamily == AddressFamily.InterNetwork) && !IPAddress.IsLoopback(localEP.Address))\n                        {\n                            //found address to use as dns server\n                            _dnsServers = new IPAddress[] { localEP.Address };\n                            return;\n                        }\n                    }\n\n                    //no useable address was found\n                    _dnsServers = null;\n                    return;\n                }\n            }\n\n            NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();\n\n            //find interface in current scope network range\n            foreach (NetworkInterface nic in networkInterfaces)\n            {\n                if (nic.OperationalStatus != OperationalStatus.Up)\n                    continue;\n\n                IPInterfaceProperties ipInterface = nic.GetIPProperties();\n\n                foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses)\n                {\n                    if (ip.Address.AddressFamily == AddressFamily.InterNetwork)\n                    {\n                        uint addressNumber = ip.Address.ConvertIpToNumber();\n\n                        if ((addressNumber & subnetMaskNumber) == networkAddressNumber)\n                        {\n                            //found address in this scope range to use as dns server\n                            _dnsServers = new IPAddress[] { ip.Address };\n                            return;\n                        }\n                    }\n                }\n            }\n\n            //find unicast ip address on an interface which has gateway\n            foreach (NetworkInterface nic in networkInterfaces)\n            {\n                if (nic.OperationalStatus != OperationalStatus.Up)\n                    continue;\n\n                IPInterfaceProperties ipInterface = nic.GetIPProperties();\n\n                if (ipInterface.GatewayAddresses.Count > 0)\n                {\n                    foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses)\n                    {\n                        if (ip.Address.AddressFamily == AddressFamily.InterNetwork)\n                        {\n                            //use this address for dns\n                            _dnsServers = new IPAddress[] { ip.Address };\n                            return;\n                        }\n                    }\n                }\n            }\n\n            //find any unicast ip address available\n            foreach (NetworkInterface nic in networkInterfaces)\n            {\n                if (nic.OperationalStatus != OperationalStatus.Up)\n                    continue;\n\n                IPInterfaceProperties ipInterface = nic.GetIPProperties();\n\n                foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses)\n                {\n                    if (ip.Address.AddressFamily == AddressFamily.InterNetwork)\n                    {\n                        //use this address for dns\n                        _dnsServers = new IPAddress[] { ip.Address };\n                        return;\n                    }\n                }\n            }\n\n            //no useable address was found\n            _dnsServers = null;\n        }\n\n        internal uint GetLeaseTime()\n        {\n            return Convert.ToUInt32((_leaseTimeDays * 24 * 60 * 60) + (_leaseTimeHours * 60 * 60) + (_leaseTimeMinutes * 60));\n        }\n\n        internal bool IsAddressInRange(IPAddress address)\n        {\n            return IsAddressInRange(address, _startingAddress, _endingAddress);\n        }\n\n        internal bool IsAddressInNetwork(IPAddress address)\n        {\n            uint addressNumber = address.ConvertIpToNumber();\n            uint networkAddressNumber = _networkAddress.ConvertIpToNumber();\n            uint broadcastAddressNumber = _broadcastAddress.ConvertIpToNumber();\n\n            return (networkAddressNumber < addressNumber) && (addressNumber < broadcastAddressNumber);\n        }\n\n        internal bool IsAddressExcluded(IPAddress address)\n        {\n            if (_exclusions != null)\n            {\n                foreach (Exclusion exclusion in _exclusions)\n                {\n                    if (IsAddressInRange(address, exclusion.StartingAddress, exclusion.EndingAddress))\n                        return true;\n                }\n            }\n\n            return false;\n        }\n\n        internal bool IsAddressReserved(IPAddress address)\n        {\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> reservedLease in _reservedLeases)\n            {\n                if (address.Equals(reservedLease.Value.Address))\n                    return true;\n            }\n\n            return false;\n        }\n\n        internal Lease GetReservedLease(DhcpMessage request)\n        {\n            return GetReservedLease(new ClientIdentifierOption((byte)request.HardwareAddressType, request.ClientHardwareAddress), request.GetClientIdentifier(_ignoreClientIdentifierOption));\n        }\n\n        private Lease GetReservedLease(ClientIdentifierOption reservedLeasesClientIdentifier, ClientIdentifierOption clientIdentifier)\n        {\n            if (_reservedLeases.TryGetValue(reservedLeasesClientIdentifier, out Lease reservedLease))\n            {\n                //reserved address exists\n                if (IsAddressAlreadyAllocated(reservedLease.Address, clientIdentifier))\n                {\n                    //reserved lease address is already allocated so ignore reserved lease\n                    _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.\");\n\n                    return null;\n                }\n\n                return reservedLease;\n            }\n\n            return null;\n        }\n\n        internal async Task<Lease> GetOfferAsync(DhcpMessage request)\n        {\n            ClientIdentifierOption clientIdentifier = request.GetClientIdentifier(_ignoreClientIdentifierOption);\n\n            if (_leases.TryGetValue(clientIdentifier, out Lease existingLease))\n            {\n                //lease already exists\n                if (existingLease.Type == LeaseType.Reserved)\n                {\n                    Lease existingReservedLease = GetReservedLease(request);\n                    if ((existingReservedLease is not null) && (existingReservedLease.Address == existingLease.Address))\n                        return existingLease; //return existing reserved lease\n\n                    //reserved lease address was changed; proceed to offer new lease\n                }\n                else\n                {\n                    //is dynamic lease\n                    if (IsAddressExcluded(existingLease.Address))\n                    {\n                        //remove existing dynamic lease; proceed to offer new lease\n                        ReleaseLease(existingLease);\n                    }\n                    else\n                    {\n                        if (_blockLocallyAdministeredMacAddresses)\n                        {\n                            if ((request.HardwareAddressType == DhcpMessageHardwareAddressType.Ethernet) && ((request.ClientHardwareAddress[0] & 0x02) > 0))\n                            {\n                                _log.Write(\"DHCP Server failed to offer IP address to \" + request.GetClientFullIdentifier() + \" for scope '\" + _name + \"': the scope does not allow locally administered MAC addresses.\");\n\n                                //prevent renewing existing dynamic lease\n                                return null;\n                            }\n                        }\n\n                        //return existing dynamic lease\n                        return existingLease;\n                    }\n                }\n            }\n\n            Lease reservedLease = GetReservedLease(request);\n            if (reservedLease != null)\n            {\n                Lease reservedOffer = new Lease(LeaseType.Reserved, clientIdentifier, null, request.ClientHardwareAddress, reservedLease.Address, null, GetLeaseTime());\n                _offers[clientIdentifier] = reservedOffer;\n                return reservedOffer;\n            }\n\n            if (_allowOnlyReservedLeases)\n            {\n                _log.Write(\"DHCP Server failed to offer IP address to \" + request.GetClientFullIdentifier() + \" for scope '\" + _name + \"': the scope allows only reserved lease allocations.\");\n\n                return null;\n            }\n\n            if (_blockLocallyAdministeredMacAddresses)\n            {\n                if ((request.HardwareAddressType == DhcpMessageHardwareAddressType.Ethernet) && ((request.ClientHardwareAddress[0] & 0x02) > 0))\n                {\n                    _log.Write(\"DHCP Server failed to offer IP address to \" + request.GetClientFullIdentifier() + \" for scope '\" + _name + \"': the scope does not allow locally administered MAC addresses.\");\n\n                    return null;\n                }\n            }\n\n            Lease dummyOffer = new Lease(LeaseType.None, null, null, null, null, null, 0);\n            Lease existingOffer = _offers.GetOrAdd(clientIdentifier, dummyOffer);\n\n            if (dummyOffer != existingOffer)\n            {\n                if (existingOffer.Type == LeaseType.None)\n                    return null; //dummy offer so another thread is handling offer; do nothing\n\n                //offer already exists\n                existingOffer.ExtendLease(GetLeaseTime());\n\n                return existingOffer;\n            }\n\n            //find offer ip address\n            IPAddress offerAddress = null;\n\n            if (request.RequestedIpAddress != null)\n            {\n                //client wish to get this address\n                IPAddress requestedAddress = request.RequestedIpAddress.Address;\n\n                if (IsAddressInRange(requestedAddress))\n                {\n                    AddressStatus addressStatus = await IsAddressAvailableAsync(requestedAddress);\n                    if (addressStatus.IsAddressAvailable)\n                        offerAddress = requestedAddress;\n                }\n            }\n\n            if (offerAddress is null)\n            {\n                await _lastAddressOfferedLock.WaitAsync();\n                try\n                {\n                    //find free address from scope\n                    offerAddress = _lastAddressOffered;\n                    uint endingAddressNumber = _endingAddress.ConvertIpToNumber();\n                    bool offerAddressWasResetFromEnd = false;\n\n                    while (true)\n                    {\n                        uint nextOfferAddressNumber = offerAddress.ConvertIpToNumber() + 1u;\n\n                        if (nextOfferAddressNumber > endingAddressNumber)\n                        {\n                            if (offerAddressWasResetFromEnd)\n                            {\n                                _log.Write(\"DHCP Server failed to offer IP address to \" + request.GetClientFullIdentifier() + \" for scope '\" + _name + \"': address unavailable due to address pool exhaustion.\");\n\n                                return null;\n                            }\n\n                            offerAddress = IPAddressExtensions.ConvertNumberToIp(_startingAddress.ConvertIpToNumber() - 1u);\n                            offerAddressWasResetFromEnd = true;\n                            continue;\n                        }\n\n                        offerAddress = IPAddressExtensions.ConvertNumberToIp(nextOfferAddressNumber);\n\n                        AddressStatus addressStatus = await IsAddressAvailableAsync(offerAddress);\n                        if (addressStatus.IsAddressAvailable)\n                            break;\n\n                        if (addressStatus.NewAddress is not null)\n                            offerAddress = addressStatus.NewAddress;\n                    }\n\n                    _lastAddressOffered = offerAddress;\n                }\n                finally\n                {\n                    _lastAddressOfferedLock.Release();\n                }\n            }\n\n            Lease offerLease = new Lease(LeaseType.Dynamic, clientIdentifier, null, request.ClientHardwareAddress, offerAddress, null, GetLeaseTime());\n            return _offers[clientIdentifier] = offerLease;\n        }\n\n        internal Lease GetExistingLeaseOrOffer(DhcpMessage request)\n        {\n            ClientIdentifierOption clientIdentifier = request.GetClientIdentifier(_ignoreClientIdentifierOption);\n\n            //check for lease offer first since it may have a different IP address to offer\n            if (_offers.TryGetValue(clientIdentifier, out Lease existingOffer))\n                return existingOffer;\n\n            if (_leases.TryGetValue(clientIdentifier, out Lease existingLease))\n                return existingLease;\n\n            return null;\n        }\n\n        internal async Task<List<DhcpOption>> GetOptionsAsync(DhcpMessage request, IPAddress serverIdentifierAddress, string reservedLeaseHostName, DnsServer dnsServer)\n        {\n            List<DhcpOption> options = new List<DhcpOption>();\n\n            switch (request.DhcpMessageType.Type)\n            {\n                case DhcpMessageType.Discover:\n                    options.Add(new DhcpMessageTypeOption(DhcpMessageType.Offer));\n                    break;\n\n                case DhcpMessageType.Request:\n                case DhcpMessageType.Inform:\n                    options.Add(new DhcpMessageTypeOption(DhcpMessageType.Ack));\n                    break;\n\n                default:\n                    return null;\n            }\n\n            options.Add(new ServerIdentifierOption(serverIdentifierAddress));\n\n            switch (request.DhcpMessageType.Type)\n            {\n                case DhcpMessageType.Discover:\n                case DhcpMessageType.Request:\n                    uint leaseTime = GetLeaseTime();\n\n                    options.Add(new IpAddressLeaseTimeOption(leaseTime));\n                    options.Add(new RenewalTimeValueOption(leaseTime / 2));\n                    options.Add(new RebindingTimeValueOption(Convert.ToUInt32(leaseTime * 0.875)));\n                    break;\n            }\n\n            if (request.ParameterRequestList is null)\n            {\n                options.Add(new SubnetMaskOption(_subnetMask));\n                options.Add(new BroadcastAddressOption(_broadcastAddress));\n\n                if (!string.IsNullOrEmpty(_domainName))\n                {\n                    options.Add(new DomainNameOption(_domainName));\n\n                    if (request.ClientFullyQualifiedDomainName != null)\n                        options.Add(GetClientFullyQualifiedDomainNameOption(request, reservedLeaseHostName));\n                }\n\n                if (_domainSearchList is not null)\n                    options.Add(new DomainSearchOption(_domainSearchList));\n\n                if (_routerAddress is not null)\n                    options.Add(new RouterOption(new IPAddress[] { _routerAddress }));\n\n                if (_dnsServers is not null)\n                    options.Add(new DomainNameServerOption(_dnsServers));\n\n                if (_winsServers is not null)\n                    options.Add(new NetBiosNameServerOption(_winsServers));\n\n                if ((_ntpServers is not null) || (_ntpServerDomainNames is not null))\n                    options.Add(await GetNetworkTimeProtocolServersOptionAsync(dnsServer));\n\n                if (_staticRoutes is not null)\n                    options.Add(new ClasslessStaticRouteOption(_staticRoutes));\n            }\n            else\n            {\n                foreach (DhcpOptionCode optionCode in request.ParameterRequestList.OptionCodes)\n                {\n                    switch (optionCode)\n                    {\n                        case DhcpOptionCode.SubnetMask:\n                            options.Add(new SubnetMaskOption(_subnetMask));\n                            options.Add(new BroadcastAddressOption(_broadcastAddress));\n                            break;\n\n                        case DhcpOptionCode.HostName:\n                            if (!string.IsNullOrWhiteSpace(reservedLeaseHostName))\n                                options.Add(new HostNameOption(reservedLeaseHostName));\n\n                            break;\n\n                        case DhcpOptionCode.DomainName:\n                            if (!string.IsNullOrEmpty(_domainName))\n                            {\n                                options.Add(new DomainNameOption(_domainName));\n\n                                if (request.ClientFullyQualifiedDomainName != null)\n                                    options.Add(GetClientFullyQualifiedDomainNameOption(request, reservedLeaseHostName));\n                            }\n\n                            break;\n\n                        case DhcpOptionCode.DomainSearch:\n                            if (_domainSearchList is not null)\n                                options.Add(new DomainSearchOption(_domainSearchList));\n\n                            break;\n\n                        case DhcpOptionCode.Router:\n                            if (_routerAddress is not null)\n                                options.Add(new RouterOption(new IPAddress[] { _routerAddress }));\n\n                            break;\n\n                        case DhcpOptionCode.DomainNameServer:\n                            if (_dnsServers is not null)\n                                options.Add(new DomainNameServerOption(_dnsServers));\n\n                            break;\n\n                        case DhcpOptionCode.NetBiosOverTcpIpNameServer:\n                            if (_winsServers is not null)\n                                options.Add(new NetBiosNameServerOption(_winsServers));\n\n                            break;\n\n                        case DhcpOptionCode.NetworkTimeProtocolServers:\n                            if ((_ntpServers is not null) || (_ntpServerDomainNames is not null))\n                                options.Add(await GetNetworkTimeProtocolServersOptionAsync(dnsServer));\n\n                            break;\n\n                        case DhcpOptionCode.ClasslessStaticRoute:\n                            if (_staticRoutes is not null)\n                                options.Add(new ClasslessStaticRouteOption(_staticRoutes));\n\n                            break;\n\n                        case DhcpOptionCode.CAPWAPAccessControllerAddresses:\n                            if (_capwapAcIpAddresses is not null)\n                                options.Add(new CAPWAPAccessControllerOption(_capwapAcIpAddresses));\n\n                            break;\n\n                        case DhcpOptionCode.TftpServerAddress:\n                            if (_tftpServerAddreses is not null)\n                                options.Add(new TftpServerAddressOption(_tftpServerAddreses));\n\n                            break;\n\n                        default:\n                            if (_genericOptions is not null)\n                            {\n                                foreach (DhcpOption genericOption in _genericOptions)\n                                {\n                                    if (optionCode == genericOption.Code)\n                                    {\n                                        options.Add(genericOption);\n                                        break;\n                                    }\n                                }\n                            }\n\n                            break;\n                    }\n                }\n            }\n\n            if ((_vendorInfo is not null) && (request.VendorClassIdentifier is not null))\n            {\n                if (_vendorInfo.TryGetValue(request.VendorClassIdentifier.Identifier, out VendorSpecificInformationOption vendorSpecificInformationOption) || _vendorInfo.TryGetValue(\"\", out vendorSpecificInformationOption))\n                {\n                    options.Add(new VendorClassIdentifierOption(request.VendorClassIdentifier.Identifier));\n                    options.Add(vendorSpecificInformationOption);\n                }\n                else\n                {\n                    string match = \"substring(vendor-class-identifier,\";\n\n                    foreach (KeyValuePair<string, VendorSpecificInformationOption> entry in _vendorInfo)\n                    {\n                        if (entry.Key.StartsWith(match))\n                        {\n                            int i = entry.Key.IndexOf(')', match.Length);\n                            if (i < match.Length)\n                                continue;\n\n                            string[] parts = entry.Key.Substring(match.Length, i - match.Length).Split(',');\n\n                            if (parts.Length != 2)\n                                continue;\n\n                            if (!int.TryParse(parts[0], out int startIndex))\n                                continue;\n\n                            if (!int.TryParse(parts[1], out int length))\n                                continue;\n\n                            if ((startIndex + length) > request.VendorClassIdentifier.Identifier.Length)\n                                continue;\n\n                            int j = entry.Key.IndexOf(\"==\", i);\n                            if (j < i)\n                                continue;\n\n                            string value = entry.Key.Substring(j + 2);\n                            value = value.Trim();\n                            value = value.Trim('\"');\n\n                            if (request.VendorClassIdentifier.Identifier.Substring(startIndex, length).Equals(value))\n                            {\n                                options.Add(new VendorClassIdentifierOption(value));\n                                options.Add(entry.Value);\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n\n            options.Add(DhcpOption.CreateEndOption());\n\n            return options;\n        }\n\n        private async Task<NetworkTimeProtocolServersOption> GetNetworkTimeProtocolServersOptionAsync(DnsServer dnsServer)\n        {\n            if (_ntpServerDomainNames is not null)\n            {\n                Task<DnsDatagram>[] tasks = new Task<DnsDatagram>[_ntpServerDomainNames.Count];\n                int i = 0;\n\n                foreach (string ntpServerDomainName in _ntpServerDomainNames)\n                    tasks[i++] = dnsServer.DirectQueryAsync(new DnsQuestionRecord(ntpServerDomainName, DnsResourceRecordType.A, DnsClass.IN), 1000);\n\n                List<IPAddress> ntpServers = new List<IPAddress>(_ntpServerDomainNames.Count + (_ntpServers is null ? 0 : _ntpServers.Count));\n\n                if (_ntpServers is not null)\n                    ntpServers.AddRange(_ntpServers);\n\n                foreach (Task<DnsDatagram> task in tasks)\n                {\n                    try\n                    {\n                        ntpServers.AddRange(DnsClient.ParseResponseA(await task));\n                    }\n                    catch\n                    { }\n                }\n\n                return new NetworkTimeProtocolServersOption(ntpServers);\n            }\n            else\n            {\n                return new NetworkTimeProtocolServersOption(_ntpServers);\n            }\n        }\n\n        internal void CommitLease(Lease lease)\n        {\n            lease.ExtendLease(GetLeaseTime());\n\n            _leases[lease.ClientIdentifier] = lease;\n            _offers.TryRemove(lease.ClientIdentifier, out _);\n\n            _lastModified = DateTime.UtcNow;\n        }\n\n        internal void ReleaseLease(Lease lease)\n        {\n            _leases.TryRemove(lease.ClientIdentifier, out _);\n\n            _lastModified = DateTime.UtcNow;\n        }\n\n        internal void SetEnabled(bool enabled)\n        {\n            _enabled = enabled;\n\n            if (!enabled)\n            {\n                _interfaceAddress = null;\n                _interfaceIndex = 0;\n            }\n        }\n\n        internal void RemoveExpiredOffers()\n        {\n            DateTime utcNow = DateTime.UtcNow;\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> offer in _offers)\n            {\n                if (utcNow > offer.Value.LeaseObtained.AddSeconds(OFFER_EXPIRY_SECONDS))\n                {\n                    //offer expired\n                    _offers.TryRemove(offer.Key, out _);\n                }\n            }\n        }\n\n        internal List<Lease> RemoveExpiredLeases()\n        {\n            List<Lease> expiredLeases = new List<Lease>();\n            DateTime utcNow = DateTime.UtcNow;\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in _leases)\n            {\n                if (utcNow > lease.Value.LeaseExpires)\n                {\n                    //lease expired\n                    if (_leases.TryRemove(lease.Key, out Lease expiredLease))\n                        expiredLeases.Add(expiredLease);\n                }\n            }\n\n            if (expiredLeases.Count > 0)\n                _lastModified = DateTime.UtcNow;\n\n            return expiredLeases;\n        }\n\n        #endregion\n\n        #region public\n\n        public void ChangeNetwork(IPAddress startingAddress, IPAddress endingAddress, IPAddress subnetMask)\n        {\n            if (startingAddress.AddressFamily != AddressFamily.InterNetwork)\n                throw new ArgumentException(\"The address must be an IPv4 address: \" + startingAddress.ToString(), nameof(startingAddress));\n\n            if (endingAddress.AddressFamily != AddressFamily.InterNetwork)\n                throw new ArgumentException(\"The address must be an IPv4 address: \" + endingAddress.ToString(), nameof(endingAddress));\n\n            if (subnetMask.AddressFamily != AddressFamily.InterNetwork)\n                throw new ArgumentException(\"The address must be an IPv4 address: \" + subnetMask.ToString(), nameof(subnetMask));\n\n            uint startingAddressNumber = startingAddress.ConvertIpToNumber();\n            uint endingAddressNumber = endingAddress.ConvertIpToNumber();\n\n            if (startingAddressNumber >= endingAddressNumber)\n                throw new ArgumentException(\"Ending address must be greater than starting address.\");\n\n            _startingAddress = startingAddress;\n            _endingAddress = endingAddress;\n            _subnetMask = subnetMask;\n\n            //compute other parameters\n            uint subnetMaskNumber = _subnetMask.ConvertIpToNumber();\n            uint networkAddressNumber = startingAddressNumber & subnetMaskNumber;\n            uint broadcastAddressNumber = networkAddressNumber | ~subnetMaskNumber;\n\n            if (networkAddressNumber == startingAddressNumber)\n                throw new ArgumentException(\"Starting address cannot be same as the network address.\");\n\n            if (broadcastAddressNumber == endingAddressNumber)\n                throw new ArgumentException(\"Ending address cannot be same as the broadcast address.\");\n\n            _networkAddress = IPAddressExtensions.ConvertNumberToIp(networkAddressNumber);\n            _broadcastAddress = IPAddressExtensions.ConvertNumberToIp(broadcastAddressNumber);\n\n            _lastAddressOfferedLock.Wait();\n            try\n            {\n                _lastAddressOffered = IPAddressExtensions.ConvertNumberToIp(startingAddressNumber - 1u);\n            }\n            finally\n            {\n                _lastAddressOfferedLock.Release();\n            }\n        }\n\n        public bool AddReservedLease(Lease reservedLease)\n        {\n            return _reservedLeases.TryAdd(reservedLease.ClientIdentifier, reservedLease);\n        }\n\n        public bool RemoveReservedLease(string hardwareAddress)\n        {\n            byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress);\n            ClientIdentifierOption reservedLeaseClientIdentifier = new ClientIdentifierOption((byte)DhcpMessageHardwareAddressType.Ethernet, hardwareAddressBytes);\n\n            return _reservedLeases.TryRemove(reservedLeaseClientIdentifier, out _);\n        }\n\n        public Lease RemoveLease(string hardwareAddress)\n        {\n            byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress);\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> entry in _leases)\n            {\n                if (BinaryNumber.Equals(entry.Value.HardwareAddress, hardwareAddressBytes))\n                    return RemoveLease(entry.Key);\n            }\n\n            throw new DhcpServerException(\"No lease was found for hardware address: \" + hardwareAddress);\n        }\n\n        public Lease RemoveLease(ClientIdentifierOption clientIdentifier)\n        {\n            if (!_leases.TryRemove(clientIdentifier, out Lease removedLease))\n                throw new DhcpServerException(\"No lease was found for client identifier: \" + clientIdentifier.ToString());\n\n            if (removedLease.Type == LeaseType.Reserved)\n            {\n                //remove reserved lease\n                ClientIdentifierOption reservedLeaseClientIdentifier = new ClientIdentifierOption((byte)DhcpMessageHardwareAddressType.Ethernet, removedLease.HardwareAddress);\n                if (_reservedLeases.TryGetValue(reservedLeaseClientIdentifier, out Lease existingReservedLease))\n                {\n                    //remove reserved lease only if the IP addresses match\n                    if (existingReservedLease.Address.Equals(removedLease.Address))\n                        _reservedLeases.TryRemove(reservedLeaseClientIdentifier, out _);\n                }\n            }\n\n            //remove DNS entries if any\n            _dhcpServer.UpdateDnsAuthZone(false, this, removedLease);\n\n            return removedLease;\n        }\n\n        public void ConvertToReservedLease(string hardwareAddress)\n        {\n            byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress);\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> entry in _leases)\n            {\n                Lease lease = entry.Value;\n\n                if ((lease.Type == LeaseType.Dynamic) && BinaryNumber.Equals(lease.HardwareAddress, hardwareAddressBytes))\n                {\n                    ConvertToReservedLease(lease);\n                    return;\n                }\n            }\n\n            throw new DhcpServerException(\"No dynamic lease was found for hardware address: \" + hardwareAddress);\n        }\n\n        public void ConvertToReservedLease(ClientIdentifierOption clientIdentifier)\n        {\n            if (!_leases.TryGetValue(clientIdentifier, out Lease lease) || (lease.Type != LeaseType.Dynamic))\n                throw new DhcpServerException(\"No dynamic lease was found for client identifier: \" + clientIdentifier.ToString());\n\n            ConvertToReservedLease(lease);\n        }\n\n        public void ConvertToDynamicLease(string hardwareAddress)\n        {\n            byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress);\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> entry in _leases)\n            {\n                Lease lease = entry.Value;\n\n                if ((lease.Type == LeaseType.Reserved) && BinaryNumber.Equals(lease.HardwareAddress, hardwareAddressBytes))\n                {\n                    ConvertToDynamicLease(lease);\n                    return;\n                }\n            }\n\n            throw new DhcpServerException(\"No reserved lease was found for hardware address: \" + hardwareAddress);\n        }\n\n        public void ConvertToDynamicLease(ClientIdentifierOption clientIdentifier)\n        {\n            if (!_leases.TryGetValue(clientIdentifier, out Lease lease) || (lease.Type != LeaseType.Reserved))\n                throw new DhcpServerException(\"No reserved lease was found for client identifier: \" + clientIdentifier.ToString());\n\n            ConvertToDynamicLease(lease);\n        }\n\n        public void WriteTo(Stream s)\n        {\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"SC\"));\n            bW.Write((byte)10); //version\n\n            bW.WriteShortString(_name);\n            bW.Write(_enabled);\n            _startingAddress.WriteTo(bW);\n            _endingAddress.WriteTo(bW);\n            _subnetMask.WriteTo(bW);\n            bW.Write(_leaseTimeDays);\n            bW.Write(_leaseTimeHours);\n            bW.Write(_leaseTimeMinutes);\n            bW.Write(_offerDelayTime);\n\n            bW.Write(_pingCheckEnabled);\n            bW.Write(_pingCheckTimeout);\n            bW.Write(_pingCheckRetries);\n\n            if (string.IsNullOrWhiteSpace(_domainName))\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_domainName);\n\n            if (_domainSearchList is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_domainSearchList.Count));\n\n                foreach (string domainSearchString in _domainSearchList)\n                    bW.WriteShortString(domainSearchString);\n            }\n\n            bW.Write(_dnsUpdates);\n            bW.Write(_dnsOverwriteForDynamicLease);\n            bW.Write(_dnsTtl);\n\n            if (_serverAddress is null)\n                IPAddress.Any.WriteTo(bW);\n            else\n                _serverAddress.WriteTo(bW);\n\n            if (string.IsNullOrEmpty(_serverHostName))\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_serverHostName);\n\n            if (string.IsNullOrEmpty(_bootFileName))\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_bootFileName);\n\n            if (_routerAddress is null)\n                IPAddress.Any.WriteTo(bW);\n            else\n                _routerAddress.WriteTo(bW);\n\n            if (_useThisDnsServer)\n            {\n                bW.Write((byte)255);\n            }\n            else if (_dnsServers is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_dnsServers.Count));\n\n                foreach (IPAddress dnsServer in _dnsServers)\n                    dnsServer.WriteTo(bW);\n            }\n\n            if (_winsServers is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_winsServers.Count));\n\n                foreach (IPAddress winsServer in _winsServers)\n                    winsServer.WriteTo(bW);\n            }\n\n            if (_ntpServers is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_ntpServers.Count));\n\n                foreach (IPAddress ntpServer in _ntpServers)\n                    ntpServer.WriteTo(bW);\n            }\n\n            if (_ntpServerDomainNames is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_ntpServerDomainNames.Count));\n\n                foreach (string ntpServerDomainName in _ntpServerDomainNames)\n                    bW.WriteShortString(ntpServerDomainName);\n            }\n\n            if (_staticRoutes is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_staticRoutes.Count));\n\n                foreach (ClasslessStaticRouteOption.Route route in _staticRoutes)\n                    route.WriteTo(bW.BaseStream);\n            }\n\n            if (_vendorInfo is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_vendorInfo.Count));\n\n                foreach (KeyValuePair<string, VendorSpecificInformationOption> entry in _vendorInfo)\n                {\n                    bW.WriteShortString(entry.Key);\n                    bW.WriteBuffer(entry.Value.Information);\n                }\n            }\n\n            if (_capwapAcIpAddresses is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_capwapAcIpAddresses.Count));\n\n                foreach (IPAddress capwapAcIpAddress in _capwapAcIpAddresses)\n                    capwapAcIpAddress.WriteTo(bW);\n            }\n\n            if (_tftpServerAddreses is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_tftpServerAddreses.Count));\n\n                foreach (IPAddress tftpServerAddress in _tftpServerAddreses)\n                    tftpServerAddress.WriteTo(bW);\n            }\n\n            if (_genericOptions is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_genericOptions.Count));\n\n                foreach (DhcpOption genericOption in _genericOptions)\n                {\n                    bW.Write((byte)genericOption.Code);\n                    bW.Write(Convert.ToInt16(genericOption.RawValue.Length));\n                    bW.Write(genericOption.RawValue);\n                }\n            }\n\n            if (_exclusions is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_exclusions.Count));\n\n                foreach (Exclusion exclusion in _exclusions)\n                {\n                    exclusion.StartingAddress.WriteTo(bW);\n                    exclusion.EndingAddress.WriteTo(bW);\n                }\n            }\n\n            bW.Write(_reservedLeases.Count);\n\n            foreach (KeyValuePair<ClientIdentifierOption, Lease> reservedLease in _reservedLeases)\n                reservedLease.Value.WriteTo(bW);\n\n            bW.Write(_allowOnlyReservedLeases);\n            bW.Write(_blockLocallyAdministeredMacAddresses);\n            bW.Write(_ignoreClientIdentifierOption);\n\n            {\n                bW.Write(_leases.Count);\n\n                foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in _leases)\n                    lease.Value.WriteTo(bW);\n            }\n        }\n\n        public override bool Equals(object obj)\n        {\n            if (obj is null)\n                return false;\n\n            if (ReferenceEquals(this, obj))\n                return true;\n\n            return Equals(obj as Scope);\n        }\n\n        public bool Equals(Scope other)\n        {\n            if (other is null)\n                return false;\n\n            if (!_startingAddress.Equals(other._startingAddress))\n                return false;\n\n            if (!_endingAddress.Equals(other._endingAddress))\n                return false;\n\n            return true;\n        }\n\n        public override int GetHashCode()\n        {\n            return HashCode.Combine(_startingAddress, _endingAddress, _subnetMask);\n        }\n\n        public override string ToString()\n        {\n            return _name;\n        }\n\n        public int CompareTo(Scope other)\n        {\n            return _name.CompareTo(other._name);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Name\n        {\n            get { return _name; }\n            set\n            {\n                ValidateScopeName(value);\n                _name = value;\n            }\n        }\n\n        public bool Enabled\n        { get { return _enabled; } }\n\n        public IPAddress StartingAddress\n        { get { return _startingAddress; } }\n\n        public IPAddress EndingAddress\n        { get { return _endingAddress; } }\n\n        public IPAddress SubnetMask\n        { get { return _subnetMask; } }\n\n        public ushort LeaseTimeDays\n        {\n            get { return _leaseTimeDays; }\n            set\n            {\n                if (value > 999)\n                    throw new ArgumentOutOfRangeException(nameof(LeaseTimeDays), \"Lease time in days must be between 0 to 999.\");\n\n                _leaseTimeDays = value;\n            }\n        }\n\n        public byte LeaseTimeHours\n        {\n            get { return _leaseTimeHours; }\n            set\n            {\n                if (value > 23)\n                    throw new ArgumentOutOfRangeException(nameof(LeaseTimeHours), \"Lease time in hours must be between 0 to 23.\");\n\n                _leaseTimeHours = value;\n            }\n        }\n\n        public byte LeaseTimeMinutes\n        {\n            get { return _leaseTimeMinutes; }\n            set\n            {\n                if (value > 59)\n                    throw new ArgumentOutOfRangeException(nameof(LeaseTimeMinutes), \"Lease time in minutes must be between 0 to 59.\");\n\n                _leaseTimeMinutes = value;\n            }\n        }\n\n        public ushort OfferDelayTime\n        {\n            get { return _offerDelayTime; }\n            set { _offerDelayTime = value; }\n        }\n\n        public bool PingCheckEnabled\n        {\n            get { return _pingCheckEnabled; }\n            set { _pingCheckEnabled = value; }\n        }\n\n        public ushort PingCheckTimeout\n        {\n            get { return _pingCheckTimeout; }\n            set { _pingCheckTimeout = value; }\n        }\n\n        public byte PingCheckRetries\n        {\n            get { return _pingCheckRetries; }\n            set { _pingCheckRetries = value; }\n        }\n\n        public string DomainName\n        {\n            get { return _domainName; }\n            set\n            {\n                if (value != null)\n                    DnsClient.IsDomainNameValid(value, true);\n\n                _domainName = value;\n            }\n        }\n\n        public IReadOnlyCollection<string> DomainSearchList\n        {\n            get { return _domainSearchList; }\n            set\n            {\n                if (value is not null)\n                {\n                    foreach (string domainSearchString in value)\n                        DnsClient.IsDomainNameValid(domainSearchString, true);\n                }\n\n                _domainSearchList = value;\n            }\n        }\n\n        public bool DnsUpdates\n        {\n            get { return _dnsUpdates; }\n            set { _dnsUpdates = value; }\n        }\n\n        public bool DnsOverwriteForDynamicLease\n        {\n            get { return _dnsOverwriteForDynamicLease; }\n            set { _dnsOverwriteForDynamicLease = value; }\n        }\n\n        public uint DnsTtl\n        {\n            get { return _dnsTtl; }\n            set { _dnsTtl = value; }\n        }\n\n        public IPAddress ServerAddress\n        {\n            get { return _serverAddress; }\n            set\n            {\n                ValidateIpv4(value, nameof(ServerAddress));\n                _serverAddress = value;\n            }\n        }\n\n        public string ServerHostName\n        {\n            get { return _serverHostName; }\n            set\n            {\n                if ((value != null) && (value.Length >= 64))\n                    throw new ArgumentException(\"Server host name cannot exceed 63 bytes.\");\n\n                _serverHostName = value;\n            }\n        }\n\n        public string BootFileName\n        {\n            get { return _bootFileName; }\n            set\n            {\n                if ((value != null) && (value.Length >= 128))\n                    throw new ArgumentException(\"Boot file name cannot exceed 127 bytes.\");\n\n                _bootFileName = value;\n            }\n        }\n\n        public IPAddress RouterAddress\n        {\n            get { return _routerAddress; }\n            set\n            {\n                ValidateIpv4(value, nameof(RouterAddress));\n                _routerAddress = value;\n            }\n        }\n\n        public bool UseThisDnsServer\n        {\n            get { return _useThisDnsServer; }\n            set\n            {\n                _useThisDnsServer = value;\n\n                if (_useThisDnsServer)\n                    FindThisDnsServerAddress();\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> DnsServers\n        {\n            get { return _dnsServers; }\n            set\n            {\n                ValidateIpv4(value, nameof(DnsServers));\n                _dnsServers = value;\n\n                if ((_dnsServers != null) && _dnsServers.Count > 0)\n                    _useThisDnsServer = false;\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> WinsServers\n        {\n            get { return _winsServers; }\n            set\n            {\n                ValidateIpv4(value, nameof(WinsServers));\n                _winsServers = value;\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> NtpServers\n        {\n            get { return _ntpServers; }\n            set\n            {\n                ValidateIpv4(value, nameof(NtpServers));\n                _ntpServers = value;\n            }\n        }\n\n        public IReadOnlyCollection<string> NtpServerDomainNames\n        {\n            get { return _ntpServerDomainNames; }\n            set\n            {\n                if (value is not null)\n                {\n                    foreach (string ntpServerDomainName in value)\n                        DnsClient.IsDomainNameValid(ntpServerDomainName, true);\n                }\n\n                _ntpServerDomainNames = value;\n            }\n        }\n\n        public IReadOnlyCollection<ClasslessStaticRouteOption.Route> StaticRoutes\n        {\n            get { return _staticRoutes; }\n            set { _staticRoutes = value; }\n        }\n\n        public IReadOnlyDictionary<string, VendorSpecificInformationOption> VendorInfo\n        {\n            get { return _vendorInfo; }\n            set { _vendorInfo = value; }\n        }\n\n        public IReadOnlyCollection<IPAddress> CAPWAPAcIpAddresses\n        {\n            get { return _capwapAcIpAddresses; }\n            set\n            {\n                ValidateIpv4(value, nameof(CAPWAPAcIpAddresses));\n                _capwapAcIpAddresses = value;\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> TftpServerAddresses\n        {\n            get { return _tftpServerAddreses; }\n            set\n            {\n                ValidateIpv4(value, nameof(TftpServerAddresses));\n                _tftpServerAddreses = value;\n            }\n        }\n\n        public IReadOnlyCollection<DhcpOption> GenericOptions\n        {\n            get { return _genericOptions; }\n            set { _genericOptions = value; }\n        }\n\n        public IReadOnlyCollection<Exclusion> Exclusions\n        {\n            get { return _exclusions; }\n            set\n            {\n                if (value is null)\n                {\n                    _exclusions = null;\n                }\n                else\n                {\n                    foreach (Exclusion exclusion in value)\n                    {\n                        if (!IsAddressInRange(exclusion.StartingAddress))\n                            throw new ArgumentOutOfRangeException(nameof(Exclusions), \"Exclusion starting address must be in scope range.\");\n\n                        if (!IsAddressInRange(exclusion.EndingAddress))\n                            throw new ArgumentOutOfRangeException(nameof(Exclusions), \"Exclusion ending address must be in scope range.\");\n                    }\n\n                    _exclusions = value;\n                }\n            }\n        }\n\n        public IReadOnlyCollection<Lease> ReservedLeases\n        {\n            get\n            {\n                List<Lease> leases = new List<Lease>(_reservedLeases.Count);\n\n                foreach (KeyValuePair<ClientIdentifierOption, Lease> entry in _reservedLeases)\n                    leases.Add(entry.Value);\n\n                leases.Sort();\n                return leases;\n            }\n            set\n            {\n                if (value is null)\n                {\n                    _reservedLeases.Clear();\n                }\n                else\n                {\n                    foreach (Lease reservedLease in value)\n                    {\n                        if (!IsAddressInRange(reservedLease.Address))\n                            throw new ArgumentOutOfRangeException(nameof(ReservedLeases), \"Reserved address must be in scope range.\");\n                    }\n\n                    _reservedLeases.Clear();\n\n                    foreach (Lease reservedLease in value)\n                        _reservedLeases.TryAdd(reservedLease.ClientIdentifier, reservedLease);\n                }\n            }\n        }\n\n        public bool AllowOnlyReservedLeases\n        {\n            get { return _allowOnlyReservedLeases; }\n            set { _allowOnlyReservedLeases = value; }\n        }\n\n        public bool BlockLocallyAdministeredMacAddresses\n        {\n            get { return _blockLocallyAdministeredMacAddresses; }\n            set { _blockLocallyAdministeredMacAddresses = value; }\n        }\n\n        public bool IgnoreClientIdentifierOption\n        {\n            get { return _ignoreClientIdentifierOption; }\n            set { _ignoreClientIdentifierOption = value; }\n        }\n\n        public IReadOnlyDictionary<ClientIdentifierOption, Lease> Leases\n        { get { return _leases; } }\n\n        public IPAddress NetworkAddress\n        { get { return _networkAddress; } }\n\n        public IPAddress BroadcastAddress\n        { get { return _broadcastAddress; } }\n\n        public IPAddress InterfaceAddress\n        { get { return _interfaceAddress; } }\n\n        internal int InterfaceIndex\n        { get { return _interfaceIndex; } }\n\n        internal DateTime LastModified\n        { get { return _lastModified; } }\n\n        #endregion\n\n        class AddressStatus\n        {\n            public static readonly AddressStatus TRUE = new AddressStatus(true, null);\n            public static readonly AddressStatus FALSE = new AddressStatus(false, null);\n\n            public readonly bool IsAddressAvailable;\n            public readonly IPAddress NewAddress;\n\n            public AddressStatus(bool isAddressAvailable, IPAddress newAddress)\n            {\n                IsAddressAvailable = isAddressAvailable;\n                NewAddress = newAddress;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Applications/DnsApplication.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\nusing System.Threading.Tasks;\n\nnamespace DnsServerCore.Dns.Applications\n{\n    public sealed class DnsApplication : IDisposable\n    {\n        #region events\n\n        public event EventHandler ConfigUpdated;\n\n        #endregion\n\n        #region variables\n\n        readonly static Type _dnsApplicationInterface = typeof(IDnsApplication);\n\n        readonly IDnsServer _dnsServer;\n        readonly string _name;\n\n        readonly DnsApplicationAssemblyLoadContext _appContext;\n\n        readonly string _description;\n        readonly Version _version;\n        readonly IReadOnlyDictionary<string, IDnsApplication> _dnsApplications;\n        readonly IReadOnlyDictionary<string, IDnsAppRecordRequestHandler> _dnsAppRecordRequestHandlers;\n        readonly IReadOnlyDictionary<string, IDnsRequestController> _dnsRequestControllers;\n        readonly IReadOnlyDictionary<string, IDnsAuthoritativeRequestHandler> _dnsAuthoritativeRequestHandlers;\n        readonly IReadOnlyDictionary<string, IDnsRequestBlockingHandler> _dnsRequestBlockingHandlers;\n        readonly IReadOnlyDictionary<string, IDnsQueryLogger> _dnsQueryLoggers;\n        readonly IReadOnlyDictionary<string, IDnsQueryLogs> _dnsQueryLogs;\n        readonly IReadOnlyDictionary<string, IDnsPostProcessor> _dnsPostProcessors;\n\n        #endregion\n\n        #region constructor\n\n        public DnsApplication(IDnsServer dnsServer, string name)\n        {\n            _dnsServer = dnsServer;\n            _name = name;\n\n            _appContext = new DnsApplicationAssemblyLoadContext(_dnsServer);\n\n            //load apps\n            Dictionary<string, IDnsApplication> dnsApplications = new Dictionary<string, IDnsApplication>();\n            Dictionary<string, IDnsAppRecordRequestHandler> dnsAppRecordRequestHandlers = new Dictionary<string, IDnsAppRecordRequestHandler>(2);\n            Dictionary<string, IDnsRequestController> dnsRequestControllers = new Dictionary<string, IDnsRequestController>(1);\n            Dictionary<string, IDnsAuthoritativeRequestHandler> dnsAuthoritativeRequestHandlers = new Dictionary<string, IDnsAuthoritativeRequestHandler>(1);\n            Dictionary<string, IDnsRequestBlockingHandler> dnsRequestBlockingHandlers = new Dictionary<string, IDnsRequestBlockingHandler>(1);\n            Dictionary<string, IDnsQueryLogger> dnsQueryLoggers = new Dictionary<string, IDnsQueryLogger>(1);\n            Dictionary<string, IDnsQueryLogs> dnsQueryLogs = new Dictionary<string, IDnsQueryLogs>(1);\n            Dictionary<string, IDnsPostProcessor> dnsPostProcessors = new Dictionary<string, IDnsPostProcessor>(1);\n\n            foreach (Assembly appAssembly in _appContext.AppAssemblies)\n            {\n                try\n                {\n                    foreach (Type classType in appAssembly.ExportedTypes)\n                    {\n                        bool isDnsApp = false;\n\n                        foreach (Type interfaceType in classType.GetInterfaces())\n                        {\n                            if (interfaceType == _dnsApplicationInterface)\n                            {\n                                isDnsApp = true;\n                                break;\n                            }\n                        }\n\n                        if (isDnsApp)\n                        {\n                            try\n                            {\n                                IDnsApplication app = Activator.CreateInstance(classType) as IDnsApplication;\n\n                                dnsApplications.Add(classType.FullName, app);\n\n                                if (app is IDnsAppRecordRequestHandler appRecordHandler)\n                                    dnsAppRecordRequestHandlers.Add(classType.FullName, appRecordHandler);\n\n                                if (app is IDnsRequestController requestController)\n                                    dnsRequestControllers.Add(classType.FullName, requestController);\n\n                                if (app is IDnsAuthoritativeRequestHandler requestHandler)\n                                    dnsAuthoritativeRequestHandlers.Add(classType.FullName, requestHandler);\n\n                                if (app is IDnsRequestBlockingHandler blockingHandler)\n                                    dnsRequestBlockingHandlers.Add(classType.FullName, blockingHandler);\n\n                                if (app is IDnsQueryLogger logger)\n                                    dnsQueryLoggers.Add(classType.FullName, logger);\n\n                                if (app is IDnsQueryLogs queryLogs)\n                                    dnsQueryLogs.Add(classType.FullName, queryLogs);\n\n                                if (app is IDnsPostProcessor postProcessor)\n                                    dnsPostProcessors.Add(classType.FullName, postProcessor);\n\n                                if (_description is null)\n                                {\n                                    AssemblyDescriptionAttribute attribute = appAssembly.GetCustomAttribute<AssemblyDescriptionAttribute>();\n                                    if (attribute is not null)\n                                        _description = attribute.Description.Replace(\"\\\\n\", \"\\n\");\n                                }\n\n                                if (_version is null)\n                                    _version = appAssembly.GetName().Version;\n                            }\n                            catch (Exception ex)\n                            {\n                                _dnsServer.WriteLog(ex);\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(ex);\n                }\n            }\n\n            if (_version is null)\n            {\n                if (dnsApplications.Count > 0)\n                    _version = new Version(1, 0);\n                else\n                    _version = new Version(0, 0);\n            }\n\n            _dnsApplications = dnsApplications;\n            _dnsAppRecordRequestHandlers = dnsAppRecordRequestHandlers;\n            _dnsRequestControllers = dnsRequestControllers;\n            _dnsAuthoritativeRequestHandlers = dnsAuthoritativeRequestHandlers;\n            _dnsRequestBlockingHandlers = dnsRequestBlockingHandlers;\n            _dnsQueryLoggers = dnsQueryLoggers;\n            _dnsQueryLogs = dnsQueryLogs;\n            _dnsPostProcessors = dnsPostProcessors;\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                if (_dnsApplications is not null)\n                {\n                    foreach (KeyValuePair<string, IDnsApplication> app in _dnsApplications)\n                        app.Value.Dispose();\n                }\n\n                if (_appContext != null)\n                    _appContext.Unload();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region internal\n\n        internal async Task InitializeAsync()\n        {\n            string config = await GetConfigAsync();\n\n            foreach (KeyValuePair<string, IDnsApplication> app in _dnsApplications)\n            {\n                try\n                {\n                    await app.Value.InitializeAsync(_dnsServer, config);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(ex);\n                }\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public Task<string> GetConfigAsync()\n        {\n            string configFile = Path.Combine(_dnsServer.ApplicationFolder, \"dnsApp.config\");\n\n            if (File.Exists(configFile))\n                return File.ReadAllTextAsync(configFile);\n\n            return Task.FromResult<string>(null);\n        }\n\n        public async Task SetConfigAsync(string config)\n        {\n            string configFile = Path.Combine(_dnsServer.ApplicationFolder, \"dnsApp.config\");\n\n            foreach (KeyValuePair<string, IDnsApplication> app in _dnsApplications)\n                await app.Value.InitializeAsync(_dnsServer, config);\n\n            if (string.IsNullOrEmpty(config))\n                File.Delete(configFile);\n            else\n                await File.WriteAllTextAsync(configFile, config);\n\n            ConfigUpdated?.Invoke(this, EventArgs.Empty);\n        }\n\n        #endregion\n\n        #region properties\n\n        public IDnsServer DnsServer\n        { get { return _dnsServer; } }\n\n        public string Name\n        { get { return _name; } }\n\n        public string Description\n        { get { return _description; } }\n\n        public Version Version\n        { get { return _version; } }\n\n        public IReadOnlyDictionary<string, IDnsApplication> DnsApplications\n        { get { return _dnsApplications; } }\n\n        public IReadOnlyDictionary<string, IDnsAppRecordRequestHandler> DnsAppRecordRequestHandlers\n        { get { return _dnsAppRecordRequestHandlers; } }\n\n        public IReadOnlyDictionary<string, IDnsRequestController> DnsRequestControllers\n        { get { return _dnsRequestControllers; } }\n\n        public IReadOnlyDictionary<string, IDnsAuthoritativeRequestHandler> DnsAuthoritativeRequestHandlers\n        { get { return _dnsAuthoritativeRequestHandlers; } }\n\n        public IReadOnlyDictionary<string, IDnsRequestBlockingHandler> DnsRequestBlockingHandler\n        { get { return _dnsRequestBlockingHandlers; } }\n\n        public IReadOnlyDictionary<string, IDnsQueryLogger> DnsQueryLoggers\n        { get { return _dnsQueryLoggers; } }\n\n        public IReadOnlyDictionary<string, IDnsQueryLogs> DnsQueryLogs\n        { get { return _dnsQueryLogs; } }\n\n        public IReadOnlyDictionary<string, IDnsPostProcessor> DnsPostProcessors\n        { get { return _dnsPostProcessors; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Applications/DnsApplicationAssemblyLoadContext.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\nusing System.Runtime.Loader;\n\nnamespace DnsServerCore.Dns.Applications\n{\n    class DnsApplicationAssemblyLoadContext : AssemblyLoadContext\n    {\n        #region variables\n\n        readonly IDnsServer _dnsServer;\n\n        readonly List<Assembly> _appAssemblies;\n        readonly AssemblyDependencyResolver _dependencyResolver;\n\n        readonly Dictionary<string, IntPtr> _loadedUnmanagedDlls = new Dictionary<string, IntPtr>();\n        readonly List<string> _dllTempPaths = new List<string>();\n\n        #endregion\n\n        #region constructor\n\n        public DnsApplicationAssemblyLoadContext(IDnsServer dnsServer)\n            : base(true)\n        {\n            _dnsServer = dnsServer;\n\n            Unloading += delegate (AssemblyLoadContext obj)\n            {\n                foreach (string dllTempPath in _dllTempPaths)\n                {\n                    try\n                    {\n                        File.Delete(dllTempPath);\n                    }\n                    catch\n                    { }\n                }\n            };\n\n            //load all app assemblies\n            Dictionary<string, Assembly> appAssemblies = new Dictionary<string, Assembly>();\n\n            foreach (string depsFile in Directory.GetFiles(_dnsServer.ApplicationFolder, \"*.deps.json\", SearchOption.TopDirectoryOnly))\n            {\n                string dllFileName = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(depsFile));\n                string dllFile = Path.Combine(_dnsServer.ApplicationFolder, dllFileName + \".dll\");\n\n                try\n                {\n                    Assembly appAssembly;\n                    string pdbFile = Path.Combine(_dnsServer.ApplicationFolder, dllFileName + \".pdb\");\n\n                    if (File.Exists(pdbFile))\n                    {\n                        using (FileStream dllStream = new FileStream(dllFile, FileMode.Open, FileAccess.Read))\n                        {\n                            using (FileStream pdbStream = new FileStream(pdbFile, FileMode.Open, FileAccess.Read))\n                            {\n                                appAssembly = LoadFromStream(dllStream, pdbStream);\n                            }\n                        }\n                    }\n                    else\n                    {\n                        using (FileStream dllStream = new FileStream(dllFile, FileMode.Open, FileAccess.Read))\n                        {\n                            appAssembly = LoadFromStream(dllStream);\n                        }\n                    }\n\n                    appAssemblies.Add(dllFile, appAssembly);\n\n                    if (_dependencyResolver is null)\n                        _dependencyResolver = new AssemblyDependencyResolver(dllFile);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.WriteLog(ex);\n                }\n            }\n\n            _appAssemblies = new List<Assembly>(appAssemblies.Values);\n        }\n\n        #endregion\n\n        #region overrides\n\n        protected override Assembly Load(AssemblyName assemblyName)\n        {\n            if (_dependencyResolver is not null)\n            {\n                string resolvedPath = _dependencyResolver.ResolveAssemblyToPath(assemblyName);\n                if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))\n                {\n                    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n                        return LoadFromAssemblyPath(GetTempDllFile(resolvedPath));\n                    else\n                        return LoadFromAssemblyPath(resolvedPath);\n                }\n            }\n\n            foreach (Assembly loadedAssembly in Default.Assemblies)\n            {\n                if (assemblyName.FullName == loadedAssembly.GetName().FullName)\n                    return loadedAssembly;\n            }\n\n            return null;\n        }\n\n        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)\n        {\n            string unmanagedDllPath = null;\n\n            if (_dependencyResolver is not null)\n            {\n                string resolvedPath = _dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);\n                if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))\n                    unmanagedDllPath = resolvedPath;\n            }\n\n            if (unmanagedDllPath is null)\n            {\n                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n                {\n                    string runtime = \"win-\" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();\n                    string[] prefixes = new string[] { \"\" };\n                    string[] extensions = new string[] { \".dll\" };\n\n                    unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtime, prefixes, extensions);\n                }\n                else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n                {\n                    bool isAlpine = false;\n\n                    try\n                    {\n                        string osReleaseFile = \"/etc/os-release\";\n\n                        if (File.Exists(osReleaseFile))\n                            isAlpine = File.ReadAllText(osReleaseFile).Contains(\"alpine\", StringComparison.OrdinalIgnoreCase);\n                    }\n                    catch\n                    { }\n\n                    string runtimeAlpine = \"linux-musl-\" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();\n                    string runtimeLinux = \"linux-\" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();\n                    string[] prefixes = new string[] { \"\", \"lib\" };\n                    string[] extensions = new string[] { \".so\", \".so.1\" };\n\n                    if (isAlpine)\n                    {\n                        unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtimeAlpine, prefixes, extensions);\n                        if (unmanagedDllPath is null)\n                            unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtimeLinux, prefixes, extensions);\n                    }\n                    else\n                    {\n                        unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtimeLinux, prefixes, extensions);\n                    }\n                }\n                else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n                {\n                    string runtime = \"osx-\" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();\n                    string[] prefixes = new string[] { \"\", \"lib\" };\n                    string[] extensions = new string[] { \".dylib\" };\n\n                    unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtime, prefixes, extensions);\n                }\n\n                if (unmanagedDllPath is null)\n                    return IntPtr.Zero;\n            }\n\n            lock (_loadedUnmanagedDlls)\n            {\n                if (!_loadedUnmanagedDlls.TryGetValue(unmanagedDllPath.ToLowerInvariant(), out IntPtr value))\n                {\n                    //load the unmanaged DLL via temp file\n                    // - to allow uninstalling/updating app at runtime on Windows\n                    // - to avoid dns server crash issue when updating apps on Linux\n                    value = LoadUnmanagedDllFromPath(GetTempDllFile(unmanagedDllPath));\n\n                    _loadedUnmanagedDlls.Add(unmanagedDllPath.ToLowerInvariant(), value);\n                }\n\n                return value;\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private string GetTempDllFile(string dllFile)\n        {\n            string tempPath = Path.GetTempFileName();\n\n            using (FileStream srcFile = new FileStream(dllFile, FileMode.Open, FileAccess.Read))\n            {\n                using (FileStream dstFile = new FileStream(tempPath, FileMode.Create, FileAccess.Write))\n                {\n                    srcFile.CopyTo(dstFile);\n                }\n            }\n\n            _dllTempPaths.Add(tempPath);\n\n            return tempPath;\n        }\n\n        private string FindUnmanagedDllPath(string unmanagedDllName, string runtime, string[] prefixes, string[] extensions)\n        {\n            foreach (string prefix in prefixes)\n            {\n                foreach (string extension in extensions)\n                {\n                    string path = Path.Combine(_dnsServer.ApplicationFolder, \"runtimes\", runtime, \"native\", prefix + unmanagedDllName + extension);\n                    if (File.Exists(path))\n                        return path;\n                }\n            }\n\n            return null;\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyList<Assembly> AppAssemblies\n        { get { return _appAssemblies; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Applications/DnsApplicationManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Http.Client;\n\nnamespace DnsServerCore.Dns.Applications\n{\n    public sealed class DnsApplicationManager : IDisposable\n    {\n        #region variables\n\n        readonly static Uri APP_STORE_URI = new Uri(\"https://go.technitium.com/?id=44\");\n\n        readonly DnsServer _dnsServer;\n\n        readonly string _appsPath;\n\n        readonly ConcurrentDictionary<string, DnsApplication> _applications = new ConcurrentDictionary<string, DnsApplication>();\n\n        IReadOnlyList<IDnsRequestController> _dnsRequestControllers = [];\n        IReadOnlyList<IDnsAuthoritativeRequestHandler> _dnsAuthoritativeRequestHandlers = [];\n        IReadOnlyList<IDnsRequestBlockingHandler> _dnsRequestBlockingHandlers = [];\n        IReadOnlyList<IDnsQueryLogger> _dnsQueryLoggers = [];\n        IReadOnlyList<IDnsPostProcessor> _dnsPostProcessors = [];\n\n        string _storeAppsJsonData;\n        DateTime _storeAppsJsonDataUpdatedOn;\n        const int STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS = 900;\n\n        Timer _appUpdateTimer;\n        const int APP_UPDATE_TIMER_INITIAL_INTERVAL = 10000;\n        const int APP_UPDATE_TIMER_PERIODIC_INTERVAL = 86400000;\n\n        #endregion\n\n        #region constructor\n\n        public DnsApplicationManager(DnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n\n            _appsPath = Path.Combine(_dnsServer.ConfigFolder, \"apps\");\n\n            if (!Directory.Exists(_appsPath))\n                Directory.CreateDirectory(_appsPath);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                _appUpdateTimer?.Dispose();\n\n                if (_applications != null)\n                    UnloadAllApplications();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task<DnsApplication> LoadApplicationAsync(string applicationFolder, bool refreshAppObjectList)\n        {\n            string applicationName = Path.GetFileName(applicationFolder);\n\n            DnsApplication application = new DnsApplication(new InternalDnsServer(_dnsServer, applicationName, applicationFolder), applicationName);\n\n            await application.InitializeAsync();\n\n            if (!_applications.TryAdd(application.Name, application))\n            {\n                application.Dispose();\n                throw new DnsServerException(\"DNS application already exists: \" + application.Name);\n            }\n\n            application.ConfigUpdated += Application_ConfigUpdated;\n\n            if (refreshAppObjectList)\n                RefreshAppObjectLists();\n\n            return application;\n        }\n\n        private void UnloadApplication(string applicationName)\n        {\n            if (!_applications.TryRemove(applicationName, out DnsApplication removedApp))\n                throw new DnsServerException(\"DNS application does not exists: \" + applicationName);\n\n            RefreshAppObjectLists();\n\n            removedApp.ConfigUpdated -= Application_ConfigUpdated;\n            removedApp.Dispose();\n        }\n\n        private void Application_ConfigUpdated(object sender, EventArgs e)\n        {\n            //refresh app objects to allow sorting them as per app preference\n            RefreshAppObjectLists();\n        }\n\n        private void RefreshAppObjectLists()\n        {\n            List<IDnsRequestController> dnsRequestControllers = new List<IDnsRequestController>(1);\n            List<IDnsAuthoritativeRequestHandler> dnsAuthoritativeRequestHandlers = new List<IDnsAuthoritativeRequestHandler>(1);\n            List<IDnsRequestBlockingHandler> dnsRequestBlockingHandlers = new List<IDnsRequestBlockingHandler>(1);\n            List<IDnsQueryLogger> dnsQueryLoggers = new List<IDnsQueryLogger>(1);\n            List<IDnsPostProcessor> dnsPostProcessors = new List<IDnsPostProcessor>(1);\n\n            foreach (KeyValuePair<string, DnsApplication> application in _applications)\n            {\n                foreach (KeyValuePair<string, IDnsRequestController> controller in application.Value.DnsRequestControllers)\n                    dnsRequestControllers.Add(controller.Value);\n\n                foreach (KeyValuePair<string, IDnsAuthoritativeRequestHandler> handler in application.Value.DnsAuthoritativeRequestHandlers)\n                    dnsAuthoritativeRequestHandlers.Add(handler.Value);\n\n                foreach (KeyValuePair<string, IDnsRequestBlockingHandler> blocker in application.Value.DnsRequestBlockingHandler)\n                    dnsRequestBlockingHandlers.Add(blocker.Value);\n\n                foreach (KeyValuePair<string, IDnsQueryLogger> logger in application.Value.DnsQueryLoggers)\n                    dnsQueryLoggers.Add(logger.Value);\n\n                foreach (KeyValuePair<string, IDnsPostProcessor> processor in application.Value.DnsPostProcessors)\n                    dnsPostProcessors.Add(processor.Value);\n            }\n\n            //sort app objects by preference\n            dnsRequestControllers.Sort(CompareApps);\n            dnsAuthoritativeRequestHandlers.Sort(CompareApps);\n            dnsRequestBlockingHandlers.Sort(CompareApps);\n            dnsQueryLoggers.Sort(CompareApps);\n            dnsPostProcessors.Sort(CompareApps);\n\n            _dnsRequestControllers = dnsRequestControllers;\n            _dnsAuthoritativeRequestHandlers = dnsAuthoritativeRequestHandlers;\n            _dnsRequestBlockingHandlers = dnsRequestBlockingHandlers;\n            _dnsQueryLoggers = dnsQueryLoggers;\n            _dnsPostProcessors = dnsPostProcessors;\n        }\n\n        private static int CompareApps<T>(T x, T y)\n        {\n            int xp;\n            int yp;\n\n            if (x is IDnsApplicationPreference xpref)\n                xp = xpref.Preference;\n            else\n                xp = 100;\n\n            if (y is IDnsApplicationPreference ypref)\n                yp = ypref.Preference;\n            else\n                yp = 100;\n\n            return xp.CompareTo(yp);\n        }\n\n        private void StartAutomaticUpdate()\n        {\n            if (_appUpdateTimer is null)\n            {\n                _appUpdateTimer = new Timer(async delegate (object state)\n                {\n                    try\n                    {\n                        if (_applications.IsEmpty)\n                            return;\n\n                        _dnsServer.LogManager.Write(\"DNS Server has started automatic update check for DNS Apps.\");\n\n                        string storeAppsJsonData = await GetStoreAppsJsonData();\n                        using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData);\n                        JsonElement jsonStoreAppsArray = jsonDocument.RootElement;\n\n                        Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version;\n\n                        foreach (DnsApplication application in _applications.Values)\n                        {\n                            foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray())\n                            {\n                                string name = jsonStoreApp.GetProperty(\"name\").GetString();\n                                if (name.Equals(application.Name))\n                                {\n                                    string url = null;\n                                    Version storeAppVersion = null;\n                                    Version lastServerVersion = null;\n\n                                    foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty(\"versions\").EnumerateArray())\n                                    {\n                                        string strServerVersion = jsonVersion.GetProperty(\"serverVersion\").GetString();\n                                        Version requiredServerVersion = new Version(strServerVersion);\n\n                                        if (currentVersion < requiredServerVersion)\n                                            continue;\n\n                                        if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion))\n                                            continue;\n\n                                        string version = jsonVersion.GetProperty(\"version\").GetString();\n                                        url = jsonVersion.GetProperty(\"url\").GetString();\n\n                                        storeAppVersion = new Version(version);\n                                        lastServerVersion = requiredServerVersion;\n                                    }\n\n                                    if ((storeAppVersion is not null) && (storeAppVersion > application.Version))\n                                    {\n                                        try\n                                        {\n                                            await DownloadAndUpdateAppAsync(application.Name, new Uri(url));\n\n                                            _dnsServer.LogManager.Write(\"DNS application '\" + application.Name + \"' was automatically updated successfully from: \" + url);\n                                        }\n                                        catch (Exception ex)\n                                        {\n                                            _dnsServer.LogManager.Write(\"Failed to automatically download and update DNS application '\" + application.Name + \"': \" + ex.ToString());\n                                        }\n                                    }\n\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                });\n\n                _appUpdateTimer.Change(APP_UPDATE_TIMER_INITIAL_INTERVAL, APP_UPDATE_TIMER_PERIODIC_INTERVAL);\n            }\n        }\n\n        private void StopAutomaticUpdate()\n        {\n            if (_appUpdateTimer is not null)\n            {\n                _appUpdateTimer.Dispose();\n                _appUpdateTimer = null;\n            }\n        }\n\n        internal async Task<string> GetStoreAppsJsonData()\n        {\n            if ((_storeAppsJsonData is null) || (DateTime.UtcNow > _storeAppsJsonDataUpdatedOn.AddSeconds(STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS)))\n            {\n                HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n                handler.Proxy = _dnsServer.Proxy;\n                handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                handler.DnsClient = _dnsServer;\n\n                using (HttpClient http = new HttpClient(handler))\n                {\n                    _storeAppsJsonData = await http.GetStringAsync(APP_STORE_URI);\n                    _storeAppsJsonDataUpdatedOn = DateTime.UtcNow;\n                }\n            }\n\n            return _storeAppsJsonData;\n        }\n\n        #endregion\n\n        #region public\n\n        public void UnloadAllApplications()\n        {\n            foreach (KeyValuePair<string, DnsApplication> application in _applications)\n            {\n                try\n                {\n                    application.Value.Dispose();\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            }\n\n            _applications.Clear();\n            _dnsRequestControllers = Array.Empty<IDnsRequestController>();\n            _dnsAuthoritativeRequestHandlers = Array.Empty<IDnsAuthoritativeRequestHandler>();\n            _dnsRequestBlockingHandlers = Array.Empty<IDnsRequestBlockingHandler>();\n            _dnsQueryLoggers = Array.Empty<IDnsQueryLogger>();\n            _dnsPostProcessors = Array.Empty<IDnsPostProcessor>();\n        }\n\n        public async Task LoadAllApplicationsAsync()\n        {\n            UnloadAllApplications();\n\n            List<Task> tasks = new List<Task>();\n\n            foreach (string applicationFolder in Directory.GetDirectories(_appsPath))\n            {\n                tasks.Add(Task.Run(async delegate ()\n                {\n                    try\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server is loading DNS application: \" + Path.GetFileName(applicationFolder));\n\n                        _ = await LoadApplicationAsync(applicationFolder, false);\n\n                        _dnsServer.LogManager.Write(\"DNS Server successfully loaded DNS application: \" + Path.GetFileName(applicationFolder));\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server failed to load DNS application: \" + Path.GetFileName(applicationFolder) + \"\\r\\n\" + ex.ToString());\n                    }\n                }));\n            }\n\n            await Task.WhenAll(tasks);\n\n            RefreshAppObjectLists();\n        }\n\n        public async Task<DnsApplication> InstallApplicationAsync(string applicationName, Stream appZipStream)\n        {\n            foreach (char invalidChar in Path.GetInvalidFileNameChars())\n            {\n                if (applicationName.Contains(invalidChar))\n                    throw new DnsServerException(\"The application name contains an invalid character: \" + invalidChar);\n            }\n\n            if (_applications.ContainsKey(applicationName))\n                throw new DnsServerException(\"DNS application already exists: \" + applicationName);\n\n            string applicationFolder = Path.Combine(_appsPath, applicationName);\n\n            if (Directory.Exists(applicationFolder))\n                Directory.Delete(applicationFolder, true);\n\n            Directory.CreateDirectory(applicationFolder);\n\n            //keep a copy of the zip file in the application folder for transferring to other nodes\n            await using (FileStream zipCopyStream = new FileStream(Path.Combine(applicationFolder, applicationName + \".zip\"), FileMode.Create, FileAccess.ReadWrite))\n            {\n                await appZipStream.CopyToAsync(zipCopyStream);\n\n                zipCopyStream.Position = 0;\n\n                using (ZipArchive appZip = new ZipArchive(zipCopyStream, ZipArchiveMode.Read, false, Encoding.UTF8))\n                {\n                    try\n                    {\n                        appZip.ExtractToDirectory(applicationFolder, true);\n\n                        return await LoadApplicationAsync(applicationFolder, true);\n                    }\n                    catch\n                    {\n                        if (Directory.Exists(applicationFolder))\n                            Directory.Delete(applicationFolder, true);\n\n                        throw;\n                    }\n                }\n            }\n        }\n\n        public async Task<DnsApplication> UpdateApplicationAsync(string applicationName, Stream appZipStream)\n        {\n            if (!_applications.ContainsKey(applicationName))\n                throw new DnsServerException(\"DNS application does not exists: \" + applicationName);\n\n            string applicationFolder = Path.Combine(_appsPath, applicationName);\n\n            //keep a copy of the zip file in the application folder for transferring to other nodes\n            await using (FileStream zipCopyStream = new FileStream(Path.Combine(applicationFolder, applicationName + \".zip\"), FileMode.Create, FileAccess.ReadWrite))\n            {\n                await appZipStream.CopyToAsync(zipCopyStream);\n\n                zipCopyStream.Position = 0;\n\n                using (ZipArchive appZip = new ZipArchive(zipCopyStream, ZipArchiveMode.Read, false, Encoding.UTF8))\n                {\n                    UnloadApplication(applicationName);\n\n                    foreach (ZipArchiveEntry entry in appZip.Entries)\n                    {\n                        string entryPath = entry.FullName;\n\n                        if (Path.DirectorySeparatorChar != '/')\n                            entryPath = entryPath.Replace('/', '\\\\');\n\n                        string filePath = Path.Combine(applicationFolder, entryPath);\n\n                        if ((entry.Name == \"dnsApp.config\") && File.Exists(filePath))\n                            continue; //avoid overwriting existing config file\n\n                        Directory.CreateDirectory(Path.GetDirectoryName(filePath));\n\n                        entry.ExtractToFile(filePath, true);\n                    }\n\n                    return await LoadApplicationAsync(applicationFolder, true);\n                }\n            }\n        }\n\n        public void UninstallApplication(string applicationName)\n        {\n            if (_applications.TryRemove(applicationName, out DnsApplication removedApp))\n            {\n                RefreshAppObjectLists();\n\n                removedApp.ConfigUpdated -= Application_ConfigUpdated;\n                removedApp.Dispose();\n\n                if (Directory.Exists(removedApp.DnsServer.ApplicationFolder))\n                {\n                    try\n                    {\n                        Directory.Delete(removedApp.DnsServer.ApplicationFolder, true);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                }\n            }\n        }\n\n        public async Task<DnsApplication> DownloadAndInstallAppAsync(string applicationName, Uri uri)\n        {\n            string tmpFile = Path.GetTempFileName();\n            try\n            {\n                await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                {\n                    //download to temp file\n                    HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n                    handler.Proxy = _dnsServer.Proxy;\n                    handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                    handler.DnsClient = _dnsServer;\n\n                    using (HttpClient http = new HttpClient(handler))\n                    {\n                        await using (Stream httpStream = await http.GetStreamAsync(uri))\n                        {\n                            await httpStream.CopyToAsync(fS);\n                        }\n                    }\n\n                    //install app\n                    fS.Position = 0;\n                    return await InstallApplicationAsync(applicationName, fS);\n                }\n            }\n            finally\n            {\n                try\n                {\n                    File.Delete(tmpFile);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            }\n        }\n\n        public async Task<DnsApplication> DownloadAndUpdateAppAsync(string applicationName, Uri uri)\n        {\n            string tmpFile = Path.GetTempFileName();\n            try\n            {\n                await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                {\n                    //download to temp file\n                    HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n                    handler.Proxy = _dnsServer.Proxy;\n                    handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                    handler.DnsClient = _dnsServer;\n\n                    using (HttpClient http = new HttpClient(handler))\n                    {\n                        await using (Stream httpStream = await http.GetStreamAsync(uri))\n                        {\n                            await httpStream.CopyToAsync(fS);\n                        }\n                    }\n\n                    //update app\n                    fS.Position = 0;\n                    return await UpdateApplicationAsync(applicationName, fS);\n                }\n            }\n            finally\n            {\n                try\n                {\n                    File.Delete(tmpFile);\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyDictionary<string, DnsApplication> Applications\n        { get { return _applications; } }\n\n        public IReadOnlyList<IDnsRequestController> DnsRequestControllers\n        { get { return _dnsRequestControllers; } }\n\n        public IReadOnlyList<IDnsAuthoritativeRequestHandler> DnsAuthoritativeRequestHandlers\n        { get { return _dnsAuthoritativeRequestHandlers; } }\n\n        public IReadOnlyList<IDnsRequestBlockingHandler> DnsRequestBlockingHandlers\n        { get { return _dnsRequestBlockingHandlers; } }\n\n        public IReadOnlyList<IDnsQueryLogger> DnsQueryLoggers\n        { get { return _dnsQueryLoggers; } }\n\n        public IReadOnlyList<IDnsPostProcessor> DnsPostProcessors\n        { get { return _dnsPostProcessors; } }\n\n        public bool EnableAutomaticUpdate\n        {\n            get { return _appUpdateTimer is not null; }\n            set\n            {\n                if (value)\n                    StartAutomaticUpdate();\n                else\n                    StopAutomaticUpdate();\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Applications/InternalDnsServer.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Net.Mail;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace DnsServerCore.Dns.Applications\n{\n    class InternalDnsServer : IDnsServer\n    {\n        #region variables\n\n        readonly DnsServer _dnsServer;\n        readonly string _applicationName;\n        readonly string _applicationFolder;\n\n        IDnsCache _dnsCache;\n\n        #endregion\n\n        #region constructor\n\n        public InternalDnsServer(DnsServer dnsServer, string applicationName, string applicationFolder)\n        {\n            _dnsServer = dnsServer;\n            _applicationName = applicationName;\n            _applicationFolder = applicationFolder;\n        }\n\n        #endregion\n\n        #region public\n\n        public Task<DnsDatagram> DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, CancellationToken cancellationToken = default)\n        {\n            return _dnsServer.DirectQueryAsync(question, timeout, true, cancellationToken);\n        }\n\n        public Task<DnsDatagram> DirectQueryAsync(DnsDatagram request, int timeout = 4000, CancellationToken cancellationToken = default)\n        {\n            return _dnsServer.DirectQueryAsync(request, timeout, true, cancellationToken);\n        }\n\n        public Task<DnsDatagram> ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default)\n        {\n            return DirectQueryAsync(question, cancellationToken: cancellationToken);\n        }\n\n        public void WriteLog(string message)\n        {\n            _dnsServer.LogManager.Write(\"DNS App [\" + _applicationName + \"]: \" + message);\n        }\n\n        public void WriteLog(Exception ex)\n        {\n            _dnsServer.LogManager.Write(\"DNS App [\" + _applicationName + \"]: \" + ex.ToString());\n        }\n\n        #endregion\n\n        #region properties\n\n        public string ApplicationName\n        { get { return _applicationName; } }\n\n        public string ApplicationFolder\n        { get { return _applicationFolder; } }\n\n        public string ServerDomain\n        { get { return _dnsServer.ServerDomain; } }\n\n        public MailAddress ResponsiblePerson\n        { get { return _dnsServer.ResponsiblePerson; } }\n\n        public IDnsCache DnsCache\n        {\n            get\n            {\n                if (_dnsCache is null)\n                    _dnsCache = new ResolverDnsCache(_dnsServer, true);\n\n                return _dnsCache;\n            }\n        }\n\n        public NetProxy Proxy\n        { get { return _dnsServer.Proxy; } }\n\n        public bool PreferIPv6\n        { get { return _dnsServer.PreferIPv6; } }\n\n        public ushort UdpPayloadSize\n        { get { return _dnsServer.UdpPayloadSize; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/DirectDnsClient.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.Dns\n{\n    class DirectDnsClient : DnsClient, IDnsCache\n    {\n        #region variables\n\n        readonly DnsServer _dnsServer;\n\n        #endregion\n\n        #region constructor\n\n        public DirectDnsClient(DnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n\n            //set dummy cache to avoid DnsCache from overwriting DnsResourceRecord.Tag properties which currently has GenericRecordInfo objects\n            //caching here is also not required since DNS server already does caching\n            Cache = this;\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override async Task<DnsDatagram> InternalResolveAsync(DnsDatagram request, Func<DnsDatagram, CancellationToken, Task<DnsDatagram>> getValidatedResponseAsync = null, bool doNotReorderNameServers = false, CancellationToken cancellationToken = default)\n        {\n            DnsDatagram response = await _dnsServer.DirectQueryAsync(request, Timeout, cancellationToken: cancellationToken);\n\n            //return DNSSEC validated response\n            return await getValidatedResponseAsync(response, cancellationToken);\n        }\n\n        #endregion\n\n        #region public\n\n        public Task<DnsDatagram> QueryAsync(DnsDatagram request, bool serveStale = false, bool findClosestNameServers = false, bool resetExpiry = false)\n        {\n            return Task.FromResult<DnsDatagram>(null); //no cache available\n        }\n\n        public void CacheResponse(DnsDatagram response, bool isDnssecBadCache = false, string zoneCut = null)\n        {\n            //do nothing to prevent caching\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/DnsServer.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing DnsServerCore.Dns.Applications;\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Trees;\nusing DnsServerCore.Dns.ZoneManagers;\nusing DnsServerCore.Dns.Zones;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Extensions;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.AspNetCore.StaticFiles;\nusing Microsoft.Extensions.FileProviders;\nusing Microsoft.Extensions.Logging;\nusing System;\nusing System.Buffers;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Mail;\nusing System.Net.Quic;\nusing System.Net.Security;\nusing System.Net.Sockets;\nusing System.Runtime.ExceptionServices;\nusing System.Security.Authentication;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ClientConnection;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Proxy;\nusing TechnitiumLibrary.Net.ProxyProtocol;\n\nnamespace DnsServerCore.Dns\n{\n#pragma warning disable CA2252 // This API requires opting into preview features\n#pragma warning disable CA1416 // Validate platform compatibility\n\n    public enum DnsServerRecursion : byte\n    {\n        Deny = 0,\n        Allow = 1,\n        AllowOnlyForPrivateNetworks = 2,\n        UseSpecifiedNetworkACL = 3\n    }\n\n    public enum DnsServerBlockingType : byte\n    {\n        AnyAddress = 0,\n        NxDomain = 1,\n        CustomAddress = 2\n    }\n\n    public sealed class DnsServer : IAsyncDisposable, IDisposable, IDnsClient\n    {\n        #region enum\n\n        enum ServiceState\n        {\n            Stopped = 0,\n            Starting = 1,\n            Running = 2,\n            Stopping = 3\n        }\n\n        #endregion\n\n        #region variables\n\n        readonly static char[] commaSeparator = new char[] { ',' };\n\n        internal const int MAX_CNAME_HOPS = 16;\n        internal const int SERVE_STALE_MAX_WAIT_TIME = 1800; //max time to wait before serve stale [RFC 8767]\n        const int SERVE_STALE_TIME_DIFFERENCE = 200; //200ms before client timeout [RFC 8767]\n        internal const int RECURSIVE_RESOLUTION_TIMEOUT = 60000; //max time that can be spent per recursive resolution task\n\n        static readonly IPEndPoint IPENDPOINT_ANY_0 = new IPEndPoint(IPAddress.Any, 0);\n        static readonly IReadOnlyCollection<DnsARecordData> _aRecords = [new DnsARecordData(IPAddress.Any)];\n        static readonly IReadOnlyCollection<DnsAAAARecordData> _aaaaRecords = [new DnsAAAARecordData(IPAddress.IPv6Any)];\n        static readonly List<SslApplicationProtocol> _doqApplicationProtocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol(\"doq\") };\n\n        string _serverDomain;\n        readonly string _configFolder;\n        readonly string _dohwwwFolder;\n        IReadOnlyList<IPEndPoint> _localEndPoints;\n        readonly LogManager _log;\n\n        MailAddress _defaultResponsiblePerson;\n        MailAddress _fallbackResponsiblePerson;\n\n        NameServerAddress _thisServer;\n\n        readonly List<Socket> _udpListeners = new List<Socket>();\n        readonly List<Socket> _udpProxyListeners = new List<Socket>();\n        readonly List<Socket> _tcpListeners = new List<Socket>();\n        readonly List<Socket> _tcpProxyListeners = new List<Socket>();\n        readonly List<Socket> _tlsListeners = new List<Socket>();\n        readonly List<QuicListener> _quicListeners = new List<QuicListener>();\n\n        WebApplication _dohWebService;\n\n        readonly AuthZoneManager _authZoneManager;\n        readonly AllowedZoneManager _allowedZoneManager;\n        readonly BlockedZoneManager _blockedZoneManager;\n        readonly BlockListZoneManager _blockListZoneManager;\n        readonly CacheZoneManager _cacheZoneManager;\n        readonly DnsApplicationManager _dnsApplicationManager;\n\n        readonly ResolverDnsCache _dnsCache;\n        readonly ResolverDnsCache _dnsCacheSkipDnsApps; //to prevent request reaching apps again\n        readonly StatsManager _statsManager;\n\n        IReadOnlyCollection<NetworkAddress> _zoneTransferAllowedNetworks;\n        IReadOnlyCollection<NetworkAddress> _notifyAllowedNetworks;\n        bool _preferIPv6;\n        bool _enableUdpSocketPool;\n        ushort _udpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE;\n        bool _dnssecValidation = true;\n\n        bool _eDnsClientSubnet;\n        byte _eDnsClientSubnetIPv4PrefixLength = 24;\n        byte _eDnsClientSubnetIPv6PrefixLength = 56;\n        NetworkAddress _eDnsClientSubnetIpv4Override;\n        NetworkAddress _eDnsClientSubnetIpv6Override;\n\n        //ipv4 prefix: udp, tcp\n        IReadOnlyDictionary<int, (int, int)> _qpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>()\n        {\n            { 32, (600, 600) },\n            { 24, (6000, 6000) }\n        };\n\n        //ipv6 prefix: udp, tcp\n        IReadOnlyDictionary<int, (int, int)> _qpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>()\n        {\n            { 128, (600, 600) },\n            { 64, (1200, 1200) },\n            { 56, (6000, 6000) }\n        };\n\n        int _qpmLimitSampleMinutes = 5;\n        int _qpmLimitUdpTruncationPercentage = 50; //percentage of requests that are responded with TC when QPM limit exceeds for UDP (Slip)\n        IReadOnlyCollection<NetworkAddress> _qpmLimitBypassList;\n\n        int _clientTimeout = 2000;\n        int _tcpSendTimeout = 10000;\n        int _tcpReceiveTimeout = 10000;\n        int _quicIdleTimeout = 60000;\n        int _quicMaxInboundStreams = 100;\n        int _listenBacklog = 100;\n\n        bool _enableDnsOverUdpProxy;\n        bool _enableDnsOverTcpProxy;\n        bool _enableDnsOverHttp;\n        bool _enableDnsOverTls;\n        bool _enableDnsOverHttps;\n        bool _enableDnsOverHttp3;\n        bool _enableDnsOverQuic;\n        IReadOnlyCollection<NetworkAccessControl> _reverseProxyNetworkACL;\n        int _dnsOverUdpProxyPort = 538;\n        int _dnsOverTcpProxyPort = 538;\n        int _dnsOverHttpPort = 80;\n        int _dnsOverTlsPort = 853;\n        int _dnsOverHttpsPort = 443;\n        int _dnsOverQuicPort = 853;\n        string _dnsTlsCertificatePath;\n        string _dnsTlsCertificatePassword;\n        string _dnsOverHttpRealIpHeader = \"X-Real-IP\";\n\n        Timer _tlsCertificateUpdateTimer;\n        const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000;\n        const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000;\n\n        DateTime _dnsTlsCertificateLastModifiedOn;\n        SslServerAuthenticationOptions _dotSslServerAuthenticationOptions;\n        SslServerAuthenticationOptions _doqSslServerAuthenticationOptions;\n        SslServerAuthenticationOptions _dohSslServerAuthenticationOptions;\n\n        IReadOnlyDictionary<string, TsigKey> _tsigKeys;\n\n        DnsServerRecursion _recursion;\n        IReadOnlyCollection<NetworkAccessControl> _recursionNetworkACL;\n\n        bool _randomizeName;\n        bool _qnameMinimization;\n\n        int _resolverRetries = 2;\n        int _resolverTimeout = 1500;\n        int _resolverConcurrency = 2;\n        int _resolverMaxStackCount = 16;\n\n        bool _saveCacheToDisk = true;\n        bool _serveStale = true;\n        int _serveStaleMaxWaitTime = SERVE_STALE_MAX_WAIT_TIME;\n        int _cachePrefetchEligibility = 2;\n        int _cachePrefetchTrigger = 9;\n        int _cachePrefetchSampleIntervalMinutes = 5;\n        int _cachePrefetchSampleEligibilityHitsPerHour = 30;\n\n        bool _enableBlocking = true;\n        bool _allowTxtBlockingReport = true;\n        IReadOnlyCollection<NetworkAddress> _blockingBypassList;\n        DnsServerBlockingType _blockingType = DnsServerBlockingType.NxDomain;\n        uint _blockingAnswerTtl = 30;\n        IReadOnlyCollection<DnsARecordData> _customBlockingARecords = [];\n        IReadOnlyCollection<DnsAAAARecordData> _customBlockingAAAARecords = [];\n\n        NetProxy _proxy;\n        IReadOnlyList<NameServerAddress> _forwarders;\n        bool _concurrentForwarding = true;\n        int _forwarderRetries = 3;\n        int _forwarderTimeout = 2000;\n        int _forwarderConcurrency = 2;\n\n        LogManager _resolverLog;\n        LogManager _queryLog;\n\n        Timer _cachePrefetchSamplingTimer;\n        readonly object _cachePrefetchSamplingTimerLock = new object();\n        const int CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL = 5000;\n\n        Timer _cachePrefetchRefreshTimer;\n        readonly object _cachePrefetchRefreshTimerLock = new object();\n        const int CACHE_PREFETCH_REFRESH_TIMER_INTEVAL = 10000;\n        IList<CacheRefreshSample> _cacheRefreshSampleList;\n\n        Timer _qpmLimitSamplingTimer;\n        readonly object _qpmLimitSamplingTimerLock = new object();\n        const int QPM_LIMIT_SAMPLING_TIMER_INTERVAL = 10000;\n        IReadOnlyDictionary<NetworkAddress, ValueTuple<long, long>> _qpmLimitClientSubnetStats;\n\n        readonly IndependentTaskScheduler _queryTaskScheduler = new IndependentTaskScheduler(threadName: \"QueryThreadPool\");\n\n        TaskPool _resolverTaskPool;\n        readonly IndependentTaskScheduler _resolverTaskScheduler = new IndependentTaskScheduler(priority: ThreadPriority.AboveNormal, threadName: \"ResolverThreadPool\");\n        readonly ConcurrentDictionary<string, Task<RecursiveResolveResponse>> _resolverTasks = new ConcurrentDictionary<string, Task<RecursiveResolveResponse>>(-1, 1000);\n\n        volatile ServiceState _state = ServiceState.Stopped;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        #endregion\n\n        #region constructor\n\n        static DnsServer()\n        {\n            //set min threads since the default value is too small\n            {\n                ThreadPool.GetMinThreads(out int minWorker, out int minIOC);\n\n                int minThreads = Environment.ProcessorCount * 16;\n\n                if (minWorker < minThreads)\n                    minWorker = minThreads;\n\n                if (minIOC < minThreads)\n                    minIOC = minThreads;\n\n                ThreadPool.SetMinThreads(minWorker, minIOC);\n            }\n        }\n\n        public DnsServer(string configFolder, string dohwwwFolder, LogManager log, string serverDomain = null)\n            : this(configFolder, dohwwwFolder, [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)], log, serverDomain)\n        { }\n\n        public DnsServer(string configFolder, string dohwwwFolder, IPEndPoint localEndPoint, LogManager log, string serverDomain = null)\n            : this(configFolder, dohwwwFolder, [localEndPoint], log, serverDomain)\n        { }\n\n        public DnsServer(string configFolder, string dohwwwFolder, IReadOnlyList<IPEndPoint> localEndPoints, LogManager log, string serverDomain = null)\n        {\n            if (string.IsNullOrEmpty(serverDomain))\n                serverDomain = Environment.MachineName.ToLowerInvariant();\n\n            if (!DnsClient.IsDomainNameValid(serverDomain) || IPAddress.TryParse(serverDomain, out _))\n                serverDomain = \"dns-server-1\"; //use this name instead since machine name is not a valid domain name\n\n            _serverDomain = serverDomain;\n            _configFolder = configFolder;\n            _dohwwwFolder = dohwwwFolder;\n            LocalEndPoints = localEndPoints;\n            _log = log;\n\n            ReconfigureResolverTaskPool(100);\n\n            _authZoneManager = new AuthZoneManager(this);\n            _allowedZoneManager = new AllowedZoneManager(this);\n            _blockedZoneManager = new BlockedZoneManager(this);\n            _blockListZoneManager = new BlockListZoneManager(this);\n            _cacheZoneManager = new CacheZoneManager(this);\n            _dnsApplicationManager = new DnsApplicationManager(this);\n\n            _dnsCache = new ResolverDnsCache(this, false);\n            _dnsCacheSkipDnsApps = new ResolverDnsCache(this, true); //to prevent request reaching apps again\n\n            //init stats\n            _statsManager = new StatsManager(this);\n\n            //load dns cache async\n            if (_saveCacheToDisk)\n            {\n                ThreadPool.QueueUserWorkItem(delegate (object state)\n                {\n                    try\n                    {\n                        _cacheZoneManager.LoadCacheZoneFile();\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(\"Failed to fully load DNS Cache from disk\\r\\n\" + ex.ToString());\n                    }\n                });\n            }\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveConfigFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public async ValueTask DisposeAsync()\n        {\n            if (_disposed)\n                return;\n\n            await StopAsync();\n\n            StopTlsCertificateUpdateTimer();\n\n            _authZoneManager?.Dispose();\n            _cacheZoneManager?.Dispose();\n\n            _allowedZoneManager?.Dispose();\n            _blockedZoneManager?.Dispose();\n            _blockListZoneManager?.Dispose();\n\n            _dnsApplicationManager?.Dispose();\n\n            _statsManager?.Dispose();\n\n            _resolverTaskPool?.Dispose();\n\n            _queryTaskScheduler?.Dispose();\n            _resolverTaskScheduler?.Dispose();\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                if (_pendingSave)\n                {\n                    try\n                    {\n                        SaveConfigFileInternal();\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(ex);\n                    }\n                    finally\n                    {\n                        _pendingSave = false;\n                    }\n                }\n            }\n\n            if (_saveCacheToDisk)\n            {\n                try\n                {\n                    _cacheZoneManager?.SaveCacheZoneFile();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n            }\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        public void Dispose()\n        {\n            DisposeAsync().Sync();\n        }\n\n        #endregion\n\n        #region config\n\n        public void LoadConfigFile()\n        {\n            string dnsConfigFile = Path.Combine(_configFolder, \"dns.config\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(dnsConfigFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS, false);\n                }\n\n                _log.Write(\"DNS Server config file was loaded: \" + dnsConfigFile);\n            }\n            catch (FileNotFoundException)\n            {\n                //general\n                string serverDomain = Environment.GetEnvironmentVariable(\"DNS_SERVER_DOMAIN\");\n                if (!string.IsNullOrEmpty(serverDomain))\n                    ServerDomain = serverDomain;\n\n                _dnsApplicationManager.EnableAutomaticUpdate = true;\n\n                string strPreferIPv6 = Environment.GetEnvironmentVariable(\"DNS_SERVER_PREFER_IPV6\");\n                if (!string.IsNullOrEmpty(strPreferIPv6))\n                    PreferIPv6 = bool.Parse(strPreferIPv6);\n\n                DnssecValidation = true;\n\n                EnableUdpSocketPool = Environment.OSVersion.Platform == PlatformID.Win32NT;\n\n                //optional protocols\n                string strDnsOverHttp = Environment.GetEnvironmentVariable(\"DNS_SERVER_OPTIONAL_PROTOCOL_DNS_OVER_HTTP\");\n                if (!string.IsNullOrEmpty(strDnsOverHttp))\n                    EnableDnsOverHttp = bool.Parse(strDnsOverHttp);\n\n                //recursion\n                string strRecursion = Environment.GetEnvironmentVariable(\"DNS_SERVER_RECURSION\");\n                if (!string.IsNullOrEmpty(strRecursion))\n                    Recursion = Enum.Parse<DnsServerRecursion>(strRecursion, true);\n                else\n                    Recursion = DnsServerRecursion.AllowOnlyForPrivateNetworks; //default for security reasons\n\n                string strRecursionNetworkACL = Environment.GetEnvironmentVariable(\"DNS_SERVER_RECURSION_NETWORK_ACL\");\n                if (!string.IsNullOrEmpty(strRecursionNetworkACL))\n                {\n                    RecursionNetworkACL = strRecursionNetworkACL.Split(NetworkAccessControl.Parse, ',');\n                }\n                else\n                {\n                    NetworkAddress[] recursionDeniedNetworks = null;\n                    NetworkAddress[] recursionAllowedNetworks = null;\n\n                    string strRecursionDeniedNetworks = Environment.GetEnvironmentVariable(\"DNS_SERVER_RECURSION_DENIED_NETWORKS\");\n                    if (!string.IsNullOrEmpty(strRecursionDeniedNetworks))\n                        recursionDeniedNetworks = strRecursionDeniedNetworks.Split(NetworkAddress.Parse, ',');\n\n                    string strRecursionAllowedNetworks = Environment.GetEnvironmentVariable(\"DNS_SERVER_RECURSION_ALLOWED_NETWORKS\");\n                    if (!string.IsNullOrEmpty(strRecursionAllowedNetworks))\n                        recursionAllowedNetworks = strRecursionAllowedNetworks.Split(NetworkAddress.Parse, ',');\n\n                    RecursionNetworkACL = AuthZoneInfo.ConvertDenyAllowToACL(recursionDeniedNetworks, recursionAllowedNetworks);\n                }\n\n                RandomizeName = false; //default false to allow resolving from bad name servers\n                QnameMinimization = true; //default true to enable privacy feature\n\n                //cache\n                _cacheZoneManager.MaximumEntries = 10000;\n\n                //blocking\n                string strEnableBlocking = Environment.GetEnvironmentVariable(\"DNS_SERVER_ENABLE_BLOCKING\");\n                if (!string.IsNullOrEmpty(strEnableBlocking))\n                    EnableBlocking = bool.Parse(strEnableBlocking);\n\n                string strAllowTxtBlockingReport = Environment.GetEnvironmentVariable(\"DNS_SERVER_ALLOW_TXT_BLOCKING_REPORT\");\n                if (!string.IsNullOrEmpty(strAllowTxtBlockingReport))\n                    AllowTxtBlockingReport = bool.Parse(strAllowTxtBlockingReport);\n\n                string strBlockListUrls = Environment.GetEnvironmentVariable(\"DNS_SERVER_BLOCK_LIST_URLS\");\n                if (!string.IsNullOrEmpty(strBlockListUrls))\n                    _blockListZoneManager.BlockListUrls = strBlockListUrls.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries);\n\n                //proxy & forwarders\n                string strForwarders = Environment.GetEnvironmentVariable(\"DNS_SERVER_FORWARDERS\");\n                if (!string.IsNullOrEmpty(strForwarders))\n                {\n                    DnsTransportProtocol forwarderProtocol;\n\n                    string strForwarderProtocol = Environment.GetEnvironmentVariable(\"DNS_SERVER_FORWARDER_PROTOCOL\");\n                    if (string.IsNullOrEmpty(strForwarderProtocol))\n                    {\n                        forwarderProtocol = DnsTransportProtocol.Udp;\n                    }\n                    else\n                    {\n                        forwarderProtocol = Enum.Parse<DnsTransportProtocol>(strForwarderProtocol, true);\n                        if (forwarderProtocol == DnsTransportProtocol.HttpsJson)\n                            forwarderProtocol = DnsTransportProtocol.Https;\n                    }\n\n                    Forwarders = strForwarders.Split(delegate (string value)\n                    {\n                        NameServerAddress forwarder = NameServerAddress.Parse(value);\n\n                        if (forwarder.Protocol != forwarderProtocol)\n                            forwarder = forwarder.Clone(forwarderProtocol);\n\n                        return forwarder;\n                    }, ',');\n                }\n\n                //logging\n                ResolverLogManager = _log;\n\n                string strUseLocalTime = Environment.GetEnvironmentVariable(\"DNS_SERVER_LOG_USING_LOCAL_TIME\");\n                if (!string.IsNullOrEmpty(strUseLocalTime))\n                    _log.UseLocalTime = bool.Parse(strUseLocalTime);\n\n                _statsManager.EnableInMemoryStats = false;\n                _statsManager.MaxStatFileDays = 365;\n\n                SaveConfigFileInternal();\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DNS Server encountered an error while loading DNS config file: \" + dnsConfigFile + \"\\r\\n\" + ex.ToString());\n                _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.\");\n            }\n        }\n\n        public void LoadConfig(Stream s, bool isConfigTransfer)\n        {\n            lock (_saveLock)\n            {\n                ReadConfigFrom(s, isConfigTransfer);\n\n                //save config file\n                SaveConfigFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        internal void SaveConfigFileInternal()\n        {\n            string configFile = Path.Combine(_configFolder, \"dns.config\");\n\n            using (MemoryStream mS = new MemoryStream())\n            {\n                //serialize config\n                WriteConfigTo(mS);\n\n                //write config\n                mS.Position = 0;\n\n                using (FileStream fS = new FileStream(configFile, FileMode.Create, FileAccess.Write))\n                {\n                    mS.CopyTo(fS);\n                }\n            }\n\n            _log.Write(\"DNS Server config file was saved: \" + configFile);\n        }\n\n        public void SaveConfigFile()\n        {\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void ReadConfigFrom(Stream s, bool isConfigTransfer)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"DC\") //format\n                throw new InvalidDataException(\"DNS Server config file format is invalid.\");\n\n            int version = bR.ReadByte();\n            if ((version < 1) || (version > 2))\n                throw new InvalidDataException(\"DNS Server config version not supported.\");\n\n            //general\n            string serverDomain = bR.ReadShortString();\n            if (!isConfigTransfer)\n            {\n                try\n                {\n                    ServerDomain = serverDomain;\n                }\n                catch\n                {\n                    //server domain failed validation\n                    _serverDomain = serverDomain;\n                }\n            }\n\n            {\n                IPEndPoint[] localEndPoints;\n\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    IPEndPoint[] localEPs = new IPEndPoint[count];\n\n                    for (int i = 0; i < count; i++)\n                        localEPs[i] = (IPEndPoint)EndPointExtensions.ReadFrom(bR);\n\n                    localEndPoints = localEPs;\n                }\n                else\n                {\n                    localEndPoints = [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)];\n                }\n\n                if (!isConfigTransfer)\n                    _localEndPoints = localEndPoints;\n            }\n\n            NetworkAddress[] ipv4SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n            if (!isConfigTransfer)\n                DnsClientConnection.IPv4SourceAddresses = ipv4SourceAddresses;\n\n            NetworkAddress[] ipv6SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n            if (!isConfigTransfer)\n                DnsClientConnection.IPv6SourceAddresses = ipv6SourceAddresses;\n\n            _authZoneManager.DefaultRecordTtl = bR.ReadUInt32();\n\n            if (version >= 2)\n            {\n                _authZoneManager.DefaultNsRecordTtl = bR.ReadUInt32();\n                _authZoneManager.DefaultSoaRecordTtl = bR.ReadUInt32();\n            }\n            else\n            {\n                _authZoneManager.DefaultNsRecordTtl = 14400;\n                _authZoneManager.DefaultSoaRecordTtl = 900;\n            }\n\n            string rp = bR.ReadString();\n            if (rp.Length == 0)\n                _defaultResponsiblePerson = null;\n            else\n                _defaultResponsiblePerson = new MailAddress(rp);\n\n            _authZoneManager.UseSoaSerialDateScheme = bR.ReadBoolean();\n            _authZoneManager.MinSoaRefresh = bR.ReadUInt32();\n            _authZoneManager.MinSoaRetry = bR.ReadUInt32();\n\n            _zoneTransferAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n            _notifyAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n\n            _dnsApplicationManager.EnableAutomaticUpdate = bR.ReadBoolean();\n\n            bool preferIPv6 = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _preferIPv6 = preferIPv6;\n\n            {\n                bool enableUdpSocketPool = bR.ReadBoolean();\n                if (!isConfigTransfer)\n                    _enableUdpSocketPool = enableUdpSocketPool;\n\n                int count = bR.ReadUInt16();\n                ushort[] socketPoolExcludedPorts = new ushort[count];\n\n                for (int i = 0; i < count; i++)\n                    socketPoolExcludedPorts[i] = bR.ReadUInt16();\n\n                if (!isConfigTransfer)\n                    UdpClientConnection.SocketPoolExcludedPorts = socketPoolExcludedPorts;\n            }\n\n            _udpPayloadSize = bR.ReadUInt16();\n            _dnssecValidation = bR.ReadBoolean();\n\n            _eDnsClientSubnet = bR.ReadBoolean();\n            _eDnsClientSubnetIPv4PrefixLength = bR.ReadByte();\n            _eDnsClientSubnetIPv6PrefixLength = bR.ReadByte();\n\n            if (bR.ReadBoolean())\n                _eDnsClientSubnetIpv4Override = NetworkAddress.ReadFrom(bR);\n            else\n                _eDnsClientSubnetIpv4Override = null;\n\n            if (bR.ReadBoolean())\n                _eDnsClientSubnetIpv6Override = NetworkAddress.ReadFrom(bR);\n            else\n                _eDnsClientSubnetIpv6Override = null;\n\n            {\n                int count = bR.ReadByte();\n                Dictionary<int, (int, int)> qpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>(count);\n\n                for (int i = 0; i < count; i++)\n                    qpmPrefixLimitsIPv4.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32()));\n\n                _qpmPrefixLimitsIPv4 = qpmPrefixLimitsIPv4;\n            }\n\n            {\n                int count = bR.ReadByte();\n                Dictionary<int, (int, int)> qpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>(count);\n\n                for (int i = 0; i < count; i++)\n                    qpmPrefixLimitsIPv6.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32()));\n\n                _qpmPrefixLimitsIPv6 = qpmPrefixLimitsIPv6;\n            }\n\n            _qpmLimitSampleMinutes = bR.ReadInt32();\n            _qpmLimitUdpTruncationPercentage = bR.ReadInt32();\n\n            _qpmLimitBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n\n            _clientTimeout = bR.ReadInt32();\n            _tcpSendTimeout = bR.ReadInt32();\n            _tcpReceiveTimeout = bR.ReadInt32();\n            _quicIdleTimeout = bR.ReadInt32();\n            _quicMaxInboundStreams = bR.ReadInt32();\n            _listenBacklog = bR.ReadInt32();\n            MaxConcurrentResolutionsPerCore = bR.ReadUInt16();\n\n            //optional protocols\n            bool enableDnsOverUdpProxy = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _enableDnsOverUdpProxy = enableDnsOverUdpProxy;\n\n            bool enableDnsOverTcpProxy = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _enableDnsOverTcpProxy = enableDnsOverTcpProxy;\n\n            bool enableDnsOverHttp = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _enableDnsOverHttp = enableDnsOverHttp;\n\n            bool enableDnsOverTls = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _enableDnsOverTls = enableDnsOverTls;\n\n            bool enableDnsOverHttps = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _enableDnsOverHttps = enableDnsOverHttps;\n\n            bool enableDnsOverHttp3 = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _enableDnsOverHttp3 = enableDnsOverHttp3;\n\n            bool enableDnsOverQuic = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _enableDnsOverQuic = enableDnsOverQuic;\n\n            int dnsOverUdpProxyPort = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _dnsOverUdpProxyPort = dnsOverUdpProxyPort;\n\n            int dnsOverTcpProxyPort = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _dnsOverTcpProxyPort = dnsOverTcpProxyPort;\n\n            int dnsOverHttpPort = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _dnsOverHttpPort = dnsOverHttpPort;\n\n            int dnsOverTlsPort = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _dnsOverTlsPort = dnsOverTlsPort;\n\n            int dnsOverHttpsPort = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _dnsOverHttpsPort = dnsOverHttpsPort;\n\n            int dnsOverQuicPort = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _dnsOverQuicPort = dnsOverQuicPort;\n\n            NetworkAccessControl[] reverseProxyNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR);\n            if (!isConfigTransfer)\n                _reverseProxyNetworkACL = reverseProxyNetworkACL;\n\n            string dnsTlsCertificatePath = bR.ReadShortString();\n            string dnsTlsCertificatePassword = bR.ReadShortString();\n\n            if (!isConfigTransfer)\n            {\n                _dnsTlsCertificatePath = dnsTlsCertificatePath;\n                _dnsTlsCertificatePassword = dnsTlsCertificatePassword;\n\n                if (_dnsTlsCertificatePath.Length == 0)\n                    _dnsTlsCertificatePath = null;\n\n                if (_dnsTlsCertificatePath is null)\n                {\n                    StopTlsCertificateUpdateTimer();\n                }\n                else\n                {\n                    string dnsTlsCertificateAbsolutePath = ConvertToAbsolutePath(_dnsTlsCertificatePath);\n\n                    try\n                    {\n                        LoadDnsTlsCertificate(dnsTlsCertificateAbsolutePath, _dnsTlsCertificatePassword);\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(\"DNS Server encountered an error while loading DNS Server TLS certificate: \" + dnsTlsCertificateAbsolutePath + \"\\r\\n\" + ex.ToString());\n                    }\n\n                    StartTlsCertificateUpdateTimer();\n                }\n            }\n\n            string dnsOverHttpRealIpHeader = bR.ReadShortString();\n            if (!isConfigTransfer)\n                _dnsOverHttpRealIpHeader = dnsOverHttpRealIpHeader;\n\n            //tsig\n            {\n                int count = bR.ReadByte();\n                Dictionary<string, TsigKey> tsigKeys = new Dictionary<string, TsigKey>(count);\n\n                for (int i = 0; i < count; i++)\n                {\n                    string keyName = bR.ReadShortString();\n                    string sharedSecret = bR.ReadShortString();\n                    TsigAlgorithm algorithm = (TsigAlgorithm)bR.ReadByte();\n\n                    tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithm));\n                }\n\n                _tsigKeys = tsigKeys;\n            }\n\n            //recursion\n            _recursion = (DnsServerRecursion)bR.ReadByte();\n            _recursionNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR);\n\n            _randomizeName = bR.ReadBoolean();\n            _qnameMinimization = bR.ReadBoolean();\n\n            _resolverRetries = bR.ReadInt32();\n            _resolverTimeout = bR.ReadInt32();\n            _resolverConcurrency = bR.ReadInt32();\n            _resolverMaxStackCount = bR.ReadInt32();\n\n            //cache\n            bool saveCacheToDisk = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _saveCacheToDisk = saveCacheToDisk;\n\n            bool serveStale = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _serveStale = serveStale;\n\n            uint serveStaleTtl = bR.ReadUInt32();\n            if (!isConfigTransfer)\n                _cacheZoneManager.ServeStaleTtl = serveStaleTtl;\n\n            uint serveStaleAnswerTtl = bR.ReadUInt32();\n            if (!isConfigTransfer)\n                _cacheZoneManager.ServeStaleAnswerTtl = serveStaleAnswerTtl;\n\n            uint serveStaleResetTtl = bR.ReadUInt32();\n            if (!isConfigTransfer)\n                _cacheZoneManager.ServeStaleResetTtl = serveStaleResetTtl;\n\n            int serveStaleMaxWaitTime = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _serveStaleMaxWaitTime = serveStaleMaxWaitTime;\n\n            long cacheMaximumEntries = bR.ReadInt64();\n            if (!isConfigTransfer)\n                _cacheZoneManager.MaximumEntries = cacheMaximumEntries;\n\n            uint minimumRecordTtl = bR.ReadUInt32();\n            if (!isConfigTransfer)\n                _cacheZoneManager.MinimumRecordTtl = minimumRecordTtl;\n\n            uint maximumRecordTtl = bR.ReadUInt32();\n            if (!isConfigTransfer)\n                _cacheZoneManager.MaximumRecordTtl = maximumRecordTtl;\n\n            uint negativeRecordTtl = bR.ReadUInt32();\n            if (!isConfigTransfer)\n                _cacheZoneManager.NegativeRecordTtl = negativeRecordTtl;\n\n            uint failureRecordTtl = bR.ReadUInt32();\n            if (!isConfigTransfer)\n                _cacheZoneManager.FailureRecordTtl = failureRecordTtl;\n\n            int cachePrefetchEligibility = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _cachePrefetchEligibility = cachePrefetchEligibility;\n\n            int cachePrefetchTrigger = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _cachePrefetchTrigger = cachePrefetchTrigger;\n\n            int cachePrefetchSampleIntervalMinutes = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _cachePrefetchSampleIntervalMinutes = cachePrefetchSampleIntervalMinutes;\n\n            int cachePrefetchSampleEligibilityHitsPerHour = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _cachePrefetchSampleEligibilityHitsPerHour = cachePrefetchSampleEligibilityHitsPerHour;\n\n            //blocking\n            _enableBlocking = bR.ReadBoolean();\n            _allowTxtBlockingReport = bR.ReadBoolean();\n\n            _blockingBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n\n            _blockingType = (DnsServerBlockingType)bR.ReadByte();\n\n            {\n                //read custom blocking addresses\n                List<DnsARecordData> dnsARecords = new List<DnsARecordData>();\n                List<DnsAAAARecordData> dnsAAAARecords = new List<DnsAAAARecordData>();\n\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    for (int i = 0; i < count; i++)\n                    {\n                        IPAddress customAddress = IPAddressExtensions.ReadFrom(bR);\n\n                        switch (customAddress.AddressFamily)\n                        {\n                            case AddressFamily.InterNetwork:\n                                dnsARecords.Add(new DnsARecordData(customAddress));\n                                break;\n\n                            case AddressFamily.InterNetworkV6:\n                                dnsAAAARecords.Add(new DnsAAAARecordData(customAddress));\n                                break;\n                        }\n                    }\n                }\n\n                _customBlockingARecords = dnsARecords;\n                _customBlockingAAAARecords = dnsAAAARecords;\n            }\n\n            _blockingAnswerTtl = bR.ReadUInt32();\n\n            //proxy & forwarders\n            NetProxyType proxyType = (NetProxyType)bR.ReadByte();\n            if (proxyType != NetProxyType.None)\n            {\n                string address = bR.ReadShortString();\n                int port = bR.ReadInt32();\n                NetworkCredential credential = null;\n\n                if (bR.ReadBoolean()) //credential set\n                    credential = new NetworkCredential(bR.ReadShortString(), bR.ReadShortString());\n\n                _proxy = NetProxy.CreateProxy(proxyType, address, port, credential);\n\n                int count = bR.ReadByte();\n                List<NetProxyBypassItem> bypassList = new List<NetProxyBypassItem>(count);\n\n                for (int i = 0; i < count; i++)\n                    bypassList.Add(new NetProxyBypassItem(bR.ReadShortString()));\n\n                _proxy.BypassList = bypassList;\n            }\n            else\n            {\n                _proxy = null;\n            }\n\n            {\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    NameServerAddress[] forwarders = new NameServerAddress[count];\n\n                    for (int i = 0; i < count; i++)\n                    {\n                        forwarders[i] = new NameServerAddress(bR);\n\n                        if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson)\n                            forwarders[i] = forwarders[i].Clone(DnsTransportProtocol.Https);\n                    }\n\n                    _forwarders = forwarders;\n                }\n                else\n                {\n                    _forwarders = null;\n                }\n            }\n\n            _concurrentForwarding = bR.ReadBoolean();\n            _forwarderRetries = bR.ReadInt32();\n            _forwarderTimeout = bR.ReadInt32();\n            _forwarderConcurrency = bR.ReadInt32();\n\n            //logging\n            bool ignoreResolverLogs = bR.ReadBoolean(); //ignore resolver logs\n            if (!isConfigTransfer)\n            {\n                if (ignoreResolverLogs)\n                    _resolverLog = null;\n                else\n                    _resolverLog = _log;\n            }\n\n            bool logQueries = bR.ReadBoolean(); //log all queries\n            if (!isConfigTransfer)\n            {\n                if (logQueries)\n                    _queryLog = _log;\n                else\n                    _queryLog = null;\n            }\n\n            bool enableInMemoryStats = bR.ReadBoolean();\n            if (!isConfigTransfer)\n                _statsManager.EnableInMemoryStats = enableInMemoryStats;\n\n            int maxStatFileDays = bR.ReadInt32();\n            if (!isConfigTransfer)\n                _statsManager.MaxStatFileDays = maxStatFileDays;\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"DC\")); //format\n            bW.Write((byte)2); //version\n\n            //general\n            bW.WriteShortString(_serverDomain);\n\n            {\n                bW.Write(Convert.ToByte(_localEndPoints.Count));\n\n                foreach (IPEndPoint localEP in _localEndPoints)\n                    localEP.WriteTo(bW);\n            }\n\n            AuthZoneInfo.WriteNetworkAddressesTo(DnsClientConnection.IPv4SourceAddresses, bW);\n            AuthZoneInfo.WriteNetworkAddressesTo(DnsClientConnection.IPv6SourceAddresses, bW);\n\n            bW.Write(_authZoneManager.DefaultRecordTtl);\n            bW.Write(_authZoneManager.DefaultNsRecordTtl);\n            bW.Write(_authZoneManager.DefaultSoaRecordTtl);\n\n            if (_defaultResponsiblePerson is null)\n                bW.WriteShortString(\"\");\n            else\n                bW.WriteShortString(_defaultResponsiblePerson.Address);\n\n            bW.Write(_authZoneManager.UseSoaSerialDateScheme);\n            bW.Write(_authZoneManager.MinSoaRefresh);\n            bW.Write(_authZoneManager.MinSoaRetry);\n\n            AuthZoneInfo.WriteNetworkAddressesTo(_zoneTransferAllowedNetworks, bW);\n            AuthZoneInfo.WriteNetworkAddressesTo(_notifyAllowedNetworks, bW);\n\n            bW.Write(_dnsApplicationManager.EnableAutomaticUpdate);\n\n            bW.Write(_preferIPv6);\n            bW.Write(_enableUdpSocketPool);\n\n            ushort[] socketPoolExcludedPorts = UdpClientConnection.SocketPoolExcludedPorts;\n            if (socketPoolExcludedPorts is null)\n            {\n                bW.Write(ushort.MinValue);\n            }\n            else\n            {\n                bW.Write(Convert.ToUInt16(socketPoolExcludedPorts.Length));\n\n                foreach (ushort excludedPort in socketPoolExcludedPorts)\n                    bW.Write(excludedPort);\n            }\n\n            bW.Write(_udpPayloadSize);\n            bW.Write(_dnssecValidation);\n\n            bW.Write(_eDnsClientSubnet);\n            bW.Write(_eDnsClientSubnetIPv4PrefixLength);\n            bW.Write(_eDnsClientSubnetIPv6PrefixLength);\n\n            if (_eDnsClientSubnetIpv4Override is null)\n            {\n                bW.Write(false);\n            }\n            else\n            {\n                bW.Write(true);\n                _eDnsClientSubnetIpv4Override.WriteTo(bW);\n            }\n\n            if (_eDnsClientSubnetIpv6Override is null)\n            {\n                bW.Write(false);\n            }\n            else\n            {\n                bW.Write(true);\n                _eDnsClientSubnetIpv6Override.WriteTo(bW);\n            }\n\n            if (_qpmPrefixLimitsIPv4.Count == 0)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_qpmPrefixLimitsIPv4.Count));\n\n                foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in _qpmPrefixLimitsIPv4)\n                {\n                    bW.Write(qpmPrefixLimit.Key);\n                    bW.Write(qpmPrefixLimit.Value.Item1);\n                    bW.Write(qpmPrefixLimit.Value.Item2);\n                }\n            }\n\n            if (_qpmPrefixLimitsIPv6.Count == 0)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_qpmPrefixLimitsIPv6.Count));\n\n                foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in _qpmPrefixLimitsIPv6)\n                {\n                    bW.Write(qpmPrefixLimit.Key);\n                    bW.Write(qpmPrefixLimit.Value.Item1);\n                    bW.Write(qpmPrefixLimit.Value.Item2);\n                }\n            }\n\n            bW.Write(_qpmLimitSampleMinutes);\n            bW.Write(_qpmLimitUdpTruncationPercentage);\n\n            AuthZoneInfo.WriteNetworkAddressesTo(_qpmLimitBypassList, bW);\n\n            bW.Write(_clientTimeout);\n            bW.Write(_tcpSendTimeout);\n            bW.Write(_tcpReceiveTimeout);\n            bW.Write(_quicIdleTimeout);\n            bW.Write(_quicMaxInboundStreams);\n            bW.Write(_listenBacklog);\n            bW.Write(MaxConcurrentResolutionsPerCore);\n\n            //optional protocols\n            bW.Write(_enableDnsOverUdpProxy);\n            bW.Write(_enableDnsOverTcpProxy);\n            bW.Write(_enableDnsOverHttp);\n            bW.Write(_enableDnsOverTls);\n            bW.Write(_enableDnsOverHttps);\n            bW.Write(_enableDnsOverHttp3);\n            bW.Write(_enableDnsOverQuic);\n\n            bW.Write(_dnsOverUdpProxyPort);\n            bW.Write(_dnsOverTcpProxyPort);\n            bW.Write(_dnsOverHttpPort);\n            bW.Write(_dnsOverTlsPort);\n            bW.Write(_dnsOverHttpsPort);\n            bW.Write(_dnsOverQuicPort);\n\n            AuthZoneInfo.WriteNetworkACLTo(_reverseProxyNetworkACL, bW);\n\n            if (_dnsTlsCertificatePath == null)\n                bW.WriteShortString(string.Empty);\n            else\n                bW.WriteShortString(_dnsTlsCertificatePath);\n\n            if (_dnsTlsCertificatePassword == null)\n                bW.WriteShortString(string.Empty);\n            else\n                bW.WriteShortString(_dnsTlsCertificatePassword);\n\n            bW.WriteShortString(_dnsOverHttpRealIpHeader);\n\n            //tsig\n            if (_tsigKeys is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_tsigKeys.Count));\n\n                foreach (KeyValuePair<string, TsigKey> tsigKey in _tsigKeys)\n                {\n                    bW.WriteShortString(tsigKey.Key);\n                    bW.WriteShortString(tsigKey.Value.SharedSecret);\n                    bW.Write((byte)tsigKey.Value.Algorithm);\n                }\n            }\n\n            //recursion\n            bW.Write((byte)_recursion);\n            AuthZoneInfo.WriteNetworkACLTo(_recursionNetworkACL, bW);\n\n            bW.Write(_randomizeName);\n            bW.Write(_qnameMinimization);\n\n            bW.Write(_resolverRetries);\n            bW.Write(_resolverTimeout);\n            bW.Write(_resolverConcurrency);\n            bW.Write(_resolverMaxStackCount);\n\n            //cache\n            bW.Write(_saveCacheToDisk);\n            bW.Write(_serveStale);\n            bW.Write(_cacheZoneManager.ServeStaleTtl);\n            bW.Write(_cacheZoneManager.ServeStaleAnswerTtl);\n            bW.Write(_cacheZoneManager.ServeStaleResetTtl);\n            bW.Write(_serveStaleMaxWaitTime);\n\n            bW.Write(_cacheZoneManager.MaximumEntries);\n            bW.Write(_cacheZoneManager.MinimumRecordTtl);\n            bW.Write(_cacheZoneManager.MaximumRecordTtl);\n            bW.Write(_cacheZoneManager.NegativeRecordTtl);\n            bW.Write(_cacheZoneManager.FailureRecordTtl);\n\n            bW.Write(_cachePrefetchEligibility);\n            bW.Write(_cachePrefetchTrigger);\n            bW.Write(_cachePrefetchSampleIntervalMinutes);\n            bW.Write(_cachePrefetchSampleEligibilityHitsPerHour);\n\n            //blocking\n            bW.Write(_enableBlocking);\n            bW.Write(_allowTxtBlockingReport);\n\n            AuthZoneInfo.WriteNetworkAddressesTo(_blockingBypassList, bW);\n\n            bW.Write((byte)_blockingType);\n\n            {\n                bW.Write(Convert.ToByte(_customBlockingARecords.Count + _customBlockingAAAARecords.Count));\n\n                foreach (DnsARecordData record in _customBlockingARecords)\n                    record.Address.WriteTo(bW);\n\n                foreach (DnsAAAARecordData record in _customBlockingAAAARecords)\n                    record.Address.WriteTo(bW);\n            }\n\n            bW.Write(_blockingAnswerTtl);\n\n            //proxy & forwarders\n            if (_proxy == null)\n            {\n                bW.Write((byte)NetProxyType.None);\n            }\n            else\n            {\n                bW.Write((byte)_proxy.Type);\n                bW.WriteShortString(_proxy.Address);\n                bW.Write(_proxy.Port);\n\n                NetworkCredential credential = _proxy.Credential;\n\n                if (credential == null)\n                {\n                    bW.Write(false);\n                }\n                else\n                {\n                    bW.Write(true);\n                    bW.WriteShortString(credential.UserName);\n                    bW.WriteShortString(credential.Password);\n                }\n\n                //bypass list\n                {\n                    bW.Write(Convert.ToByte(_proxy.BypassList.Count));\n\n                    foreach (NetProxyBypassItem item in _proxy.BypassList)\n                        bW.WriteShortString(item.Value);\n                }\n            }\n\n            if (_forwarders == null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_forwarders.Count));\n\n                foreach (NameServerAddress forwarder in _forwarders)\n                    forwarder.WriteTo(bW);\n            }\n\n            bW.Write(_concurrentForwarding);\n            bW.Write(_forwarderRetries);\n            bW.Write(_forwarderTimeout);\n            bW.Write(_forwarderConcurrency);\n\n            //logging\n            bW.Write(_resolverLog is null); //ignore resolver logs\n            bW.Write(_queryLog is not null); //log all queries\n            bW.Write(_statsManager.EnableInMemoryStats);\n            bW.Write(_statsManager.MaxStatFileDays);\n        }\n\n        #endregion\n\n        #region tls\n\n        private void StartTlsCertificateUpdateTimer()\n        {\n            if (_tlsCertificateUpdateTimer is null)\n            {\n                _tlsCertificateUpdateTimer = new Timer(delegate (object state)\n                {\n                    if (!string.IsNullOrEmpty(_dnsTlsCertificatePath))\n                    {\n                        string dnsTlsCertificatePath = ConvertToAbsolutePath(_dnsTlsCertificatePath);\n\n                        try\n                        {\n                            FileInfo fileInfo = new FileInfo(dnsTlsCertificatePath);\n\n                            if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _dnsTlsCertificateLastModifiedOn))\n                                LoadDnsTlsCertificate(dnsTlsCertificatePath, _dnsTlsCertificatePassword);\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(\"DNS Server encountered an error while updating DNS Server TLS Certificate: \" + dnsTlsCertificatePath + \"\\r\\n\" + ex.ToString());\n                        }\n                    }\n\n                }, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL);\n            }\n        }\n\n        private void StopTlsCertificateUpdateTimer()\n        {\n            if (_tlsCertificateUpdateTimer is not null)\n            {\n                _tlsCertificateUpdateTimer.Dispose();\n                _tlsCertificateUpdateTimer = null;\n            }\n        }\n\n        private void LoadDnsTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword)\n        {\n            FileInfo fileInfo = new FileInfo(tlsCertificatePath);\n\n            if (!fileInfo.Exists)\n                throw new ArgumentException(\"DNS Server TLS certificate file does not exists: \" + tlsCertificatePath);\n\n            switch (Path.GetExtension(tlsCertificatePath).ToLowerInvariant())\n            {\n                case \".pfx\":\n                case \".p12\":\n                    break;\n\n                default:\n                    throw new ArgumentException(\"DNS Server TLS certificate file must be PKCS #12 formatted with .pfx or .p12 extension: \" + tlsCertificatePath);\n            }\n\n            X509Certificate2Collection certificateCollection = X509CertificateLoader.LoadPkcs12CollectionFromFile(tlsCertificatePath, tlsCertificatePassword, X509KeyStorageFlags.PersistKeySet);\n            X509Certificate2 serverCertificate = null;\n\n            foreach (X509Certificate2 certificate in certificateCollection)\n            {\n                if (certificate.HasPrivateKey)\n                {\n                    serverCertificate = certificate;\n                    break;\n                }\n            }\n\n            if (serverCertificate is null)\n                throw new ArgumentException(\"DNS Server TLS certificate file must contain a certificate with private key.\");\n\n            SslStreamCertificateContext certificateContext = SslStreamCertificateContext.Create(serverCertificate, certificateCollection, false);\n\n            _dotSslServerAuthenticationOptions = new SslServerAuthenticationOptions()\n            {\n                ServerCertificateContext = certificateContext\n            };\n\n            _doqSslServerAuthenticationOptions = new SslServerAuthenticationOptions()\n            {\n                ApplicationProtocols = _doqApplicationProtocols,\n                ServerCertificateContext = certificateContext\n            };\n\n            List<SslApplicationProtocol> applicationProtocols = new List<SslApplicationProtocol>();\n\n            if (_enableDnsOverHttp3)\n                applicationProtocols.Add(new SslApplicationProtocol(\"h3\"));\n\n            if (IsHttp2Supported())\n                applicationProtocols.Add(new SslApplicationProtocol(\"h2\"));\n\n            applicationProtocols.Add(new SslApplicationProtocol(\"http/1.1\"));\n\n            _dohSslServerAuthenticationOptions = new SslServerAuthenticationOptions\n            {\n                ApplicationProtocols = applicationProtocols,\n                ServerCertificateContext = certificateContext,\n            };\n\n            _dnsTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc;\n\n            _log.Write(\"DNS Server TLS certificate was loaded: \" + tlsCertificatePath);\n        }\n\n        public void RemoveDnsTlsCertificate()\n        {\n            _dotSslServerAuthenticationOptions = null;\n            _doqSslServerAuthenticationOptions = null;\n            _dohSslServerAuthenticationOptions = null;\n\n            _dnsTlsCertificatePath = null;\n            _dnsTlsCertificatePassword = null;\n\n            StopTlsCertificateUpdateTimer();\n        }\n\n        public void SetDnsTlsCertificate(string dnsTlsCertificatePath, string dnsTlsCertificatePassword = null)\n        {\n            if (string.IsNullOrEmpty(dnsTlsCertificatePath))\n                throw new ArgumentNullException(nameof(dnsTlsCertificatePath), \"DNS optional protocols TLS certificate path cannot be null or empty.\");\n\n            if (dnsTlsCertificatePath.Length > 255)\n                throw new ArgumentException(\"DNS optional protocols TLS certificate path length cannot exceed 255 characters.\", nameof(dnsTlsCertificatePath));\n\n            if (dnsTlsCertificatePassword?.Length > 255)\n                throw new ArgumentException(\"DNS optional protocols TLS certificate password length cannot exceed 255 characters.\", nameof(dnsTlsCertificatePassword));\n\n            dnsTlsCertificatePath = ConvertToAbsolutePath(dnsTlsCertificatePath);\n\n            try\n            {\n                LoadDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword);\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DNS Server encountered an error while loading DNS Server TLS certificate: \" + dnsTlsCertificatePath + \"\\r\\n\" + ex.ToString());\n            }\n\n            _dnsTlsCertificatePath = ConvertToRelativePath(dnsTlsCertificatePath);\n            _dnsTlsCertificatePassword = dnsTlsCertificatePassword;\n\n            StartTlsCertificateUpdateTimer();\n        }\n\n        private string ConvertToRelativePath(string path)\n        {\n            if (path.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))\n                path = path.Substring(_configFolder.Length).TrimStart(Path.DirectorySeparatorChar);\n\n            return path;\n        }\n\n        private string ConvertToAbsolutePath(string path)\n        {\n            if (path is null)\n                return null;\n\n            if (Path.IsPathRooted(path))\n                return path;\n\n            return Path.Combine(_configFolder, path);\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task ReadUdpRequestAsync(Socket udpListener, DnsTransportProtocol protocol)\n        {\n            bool sendTruncationResponse;\n            byte[] recvBuffer;\n\n            if (protocol == DnsTransportProtocol.UdpProxy)\n                recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE + 256];\n            else\n                recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE];\n\n            using MemoryStream recvBufferStream = new MemoryStream(recvBuffer);\n\n            try\n            {\n                int localPort = (udpListener.LocalEndPoint as IPEndPoint).Port;\n                EndPoint epAny;\n\n                switch (udpListener.AddressFamily)\n                {\n                    case AddressFamily.InterNetwork:\n                        epAny = new IPEndPoint(IPAddress.Any, 0);\n                        break;\n\n                    case AddressFamily.InterNetworkV6:\n                        epAny = new IPEndPoint(IPAddress.IPv6Any, 0);\n                        break;\n\n                    default:\n                        throw new NotSupportedException(\"AddressFamily not supported.\");\n                }\n\n                SocketReceiveMessageFromResult result;\n\n                while (true)\n                {\n                    recvBufferStream.SetLength(DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE); //resetting length before using buffer\n\n                    try\n                    {\n                        result = await udpListener.ReceiveMessageFromAsync(recvBuffer, SocketFlags.None, epAny);\n                    }\n                    catch (SocketException ex)\n                    {\n                        switch (ex.SocketErrorCode)\n                        {\n                            case SocketError.ConnectionReset:\n                            case SocketError.HostUnreachable:\n                            case SocketError.MessageSize:\n                            case SocketError.NetworkReset:\n                                result = default;\n                                break;\n\n                            default:\n                                throw;\n                        }\n                    }\n\n                    if (result.ReceivedBytes > 0)\n                    {\n                        if (result.RemoteEndPoint is not IPEndPoint remoteEP)\n                            continue;\n\n                        try\n                        {\n                            recvBufferStream.Position = 0;\n                            recvBufferStream.SetLength(result.ReceivedBytes);\n\n                            IPEndPoint returnEP = remoteEP;\n\n                            if (protocol == DnsTransportProtocol.UdpProxy)\n                            {\n                                if (!NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL))\n                                {\n                                    //this feature is intended to be used with a reverse proxy or load balancer on private network\n                                    continue;\n                                }\n\n                                ProxyProtocolStream proxyStream = await ProxyProtocolStream.CreateAsServerAsync(recvBufferStream);\n\n                                if (!proxyStream.IsLocal)\n                                    remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort);\n\n                                recvBufferStream.Position = proxyStream.DataOffset;\n                            }\n\n                            if (HasQpmLimitExceeded(remoteEP.Address, DnsTransportProtocol.Udp))\n                            {\n                                if (SendQpmLimitExceededTruncationResponse())\n                                {\n                                    sendTruncationResponse = true;\n                                }\n                                else\n                                {\n                                    _statsManager.QueueUpdate(null, remoteEP, protocol, null, true);\n                                    continue;\n                                }\n                            }\n                            else\n                            {\n                                sendTruncationResponse = false;\n                            }\n\n                            DnsDatagram request = DnsDatagram.ReadFrom(recvBufferStream);\n                            request.SetMetadata(new NameServerAddress(new IPEndPoint(result.PacketInformation.Address, localPort), DnsTransportProtocol.Udp));\n\n                            _ = ProcessUdpRequestAsync(udpListener, remoteEP, returnEP, protocol, request, sendTruncationResponse);\n                        }\n                        catch (EndOfStreamException)\n                        {\n                            //ignore incomplete udp datagrams\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(remoteEP, protocol, ex);\n                        }\n                    }\n                }\n            }\n            catch (ObjectDisposedException)\n            {\n                //server stopped\n            }\n            catch (SocketException ex)\n            {\n                switch (ex.SocketErrorCode)\n                {\n                    case SocketError.OperationAborted:\n                    case SocketError.Interrupted:\n                        break; //server stopping\n\n                    default:\n                        if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                            return; //server stopping\n\n                        _log.Write(ex);\n                        break;\n                }\n            }\n            catch (Exception ex)\n            {\n                if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                    return; //server stopping\n\n                _log.Write(ex);\n            }\n        }\n\n        private async Task ProcessUdpRequestAsync(Socket udpListener, IPEndPoint remoteEP, IPEndPoint returnEP, DnsTransportProtocol protocol, DnsDatagram request, bool sendTruncationResponse)\n        {\n            byte[] sendBuffer = null;\n\n            try\n            {\n                bool recursionAllowed = IsRecursionAllowed(remoteEP.Address);\n                DnsDatagram response;\n\n                if (sendTruncationResponse)\n                {\n                    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 };\n                }\n                else\n                {\n                    response = await ProcessRequestAsync(request, remoteEP, protocol, recursionAllowed);\n                    if (response is null)\n                    {\n                        _statsManager.QueueUpdate(null, remoteEP, protocol, null, false);\n                        return; //drop request\n                    }\n                }\n\n                //send response\n                int sendBufferSize;\n\n                if (request.EDNS is null)\n                    sendBufferSize = 512;\n                else if (request.EDNS.UdpPayloadSize > _udpPayloadSize)\n                    sendBufferSize = _udpPayloadSize;\n                else\n                    sendBufferSize = request.EDNS.UdpPayloadSize;\n\n                sendBuffer = ArrayPool<byte>.Shared.Rent(sendBufferSize);\n\n                using (MemoryStream sendBufferStream = new MemoryStream(sendBuffer, 0, sendBufferSize))\n                {\n                    try\n                    {\n                        response.WriteTo(sendBufferStream);\n                    }\n                    catch (NotSupportedException)\n                    {\n                        if (response.IsSigned)\n                        {\n                            //rfc8945 section 5.3\n                            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 };\n                        }\n                        else\n                        {\n                            switch (response.Question[0].Type)\n                            {\n                                case DnsResourceRecordType.MX:\n                                case DnsResourceRecordType.SRV:\n                                case DnsResourceRecordType.SVCB:\n                                case DnsResourceRecordType.HTTPS:\n                                    //removing glue records and trying again since some mail servers fail to fallback to TCP on truncation\n                                    //removing glue records to prevent truncation for SRV/SVCB/HTTPS\n                                    response = response.CloneWithoutGlueRecords();\n                                    sendBufferStream.Position = 0;\n\n                                    try\n                                    {\n                                        response.WriteTo(sendBufferStream);\n                                    }\n                                    catch (NotSupportedException)\n                                    {\n                                        //send TC since response is still big even after removing glue records\n                                        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 };\n                                    }\n                                    break;\n\n                                case DnsResourceRecordType.IXFR:\n                                    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\n                                    break;\n\n                                default:\n                                    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 };\n                                    break;\n                            }\n                        }\n\n                        sendBufferStream.Position = 0;\n                        response.WriteTo(sendBufferStream);\n                    }\n\n                    //send dns datagram async\n                    await udpListener.SendToAsync(new ArraySegment<byte>(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, returnEP);\n                }\n\n                _queryLog?.Write(remoteEP, protocol, request, response);\n                _statsManager.QueueUpdate(request, remoteEP, protocol, response, false);\n            }\n            catch (ObjectDisposedException)\n            {\n                //ignore\n            }\n            catch (Exception ex)\n            {\n                if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                    return; //server stopping\n\n                _queryLog?.Write(remoteEP, protocol, request, null);\n                _log.Write(remoteEP, protocol, ex);\n            }\n            finally\n            {\n                if (sendBuffer is not null)\n                    ArrayPool<byte>.Shared.Return(sendBuffer);\n            }\n        }\n\n        private async Task AcceptConnectionAsync(Socket tcpListener, DnsTransportProtocol protocol)\n        {\n            IPEndPoint localEP = tcpListener.LocalEndPoint as IPEndPoint;\n\n            try\n            {\n                tcpListener.SendTimeout = _tcpSendTimeout;\n                tcpListener.ReceiveTimeout = _tcpReceiveTimeout;\n                tcpListener.NoDelay = true;\n\n                while (true)\n                {\n                    Socket socket = await tcpListener.AcceptAsync();\n\n                    _ = ProcessConnectionAsync(socket, protocol);\n                }\n            }\n            catch (SocketException ex)\n            {\n                if (ex.SocketErrorCode == SocketError.OperationAborted)\n                    return; //server stopping\n\n                _log.Write(localEP, protocol, ex);\n            }\n            catch (ObjectDisposedException)\n            {\n                //server stopped\n            }\n            catch (Exception ex)\n            {\n                if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                    return; //server stopping\n\n                _log.Write(localEP, protocol, ex);\n            }\n        }\n\n        private async Task ProcessConnectionAsync(Socket socket, DnsTransportProtocol protocol)\n        {\n            IPEndPoint remoteEP = null;\n\n            try\n            {\n                remoteEP = socket.RemoteEndPoint as IPEndPoint;\n\n                switch (protocol)\n                {\n                    case DnsTransportProtocol.Tcp:\n                        await ReadStreamRequestAsync(new NetworkStream(socket), remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol);\n                        break;\n\n                    case DnsTransportProtocol.Tls:\n                        SslStream tlsStream = new SslStream(new NetworkStream(socket));\n                        string serverName = null;\n\n                        await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                        {\n                            return tlsStream.AuthenticateAsServerAsync(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)\n                            {\n                                serverName = clientHelloInfo.ServerName;\n                                return ValueTask.FromResult(_dotSslServerAuthenticationOptions);\n                            }, null, cancellationToken1);\n                        }, _tcpReceiveTimeout);\n\n                        NameServerAddress dnsEP;\n\n                        if (string.IsNullOrEmpty(serverName))\n                            dnsEP = new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tls);\n                        else\n                            dnsEP = new NameServerAddress(serverName, socket.LocalEndPoint as IPEndPoint, DnsTransportProtocol.Tls);\n\n                        await ReadStreamRequestAsync(tlsStream, remoteEP, dnsEP, protocol);\n                        break;\n\n                    case DnsTransportProtocol.TcpProxy:\n                        if (!NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL))\n                        {\n                            //this feature is intended to be used with a reverse proxy or load balancer on private network\n                            return;\n                        }\n\n                        ProxyProtocolStream proxyStream = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                        {\n                            return ProxyProtocolStream.CreateAsServerAsync(new NetworkStream(socket), cancellationToken1);\n                        }, _tcpReceiveTimeout);\n\n                        remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort);\n\n                        await ReadStreamRequestAsync(proxyStream, remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol);\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n            catch (AuthenticationException)\n            {\n                //ignore TLS auth exception\n            }\n            catch (TimeoutException)\n            {\n                //ignore timeout exception on TLS auth\n            }\n            catch (IOException)\n            {\n                //ignore IO exceptions\n            }\n            catch (Exception ex)\n            {\n                _log.Write(remoteEP, protocol, ex);\n            }\n            finally\n            {\n                socket.Dispose();\n            }\n        }\n\n        private async Task ReadStreamRequestAsync(Stream stream, IPEndPoint remoteEP, NameServerAddress dnsEP, DnsTransportProtocol protocol)\n        {\n            try\n            {\n                using MemoryStream readBuffer = new MemoryStream(64);\n                using MemoryStream writeBuffer = new MemoryStream(2048);\n                using SemaphoreSlim writeSemaphore = new SemaphoreSlim(1, 1);\n\n                while (true)\n                {\n                    if (HasQpmLimitExceeded(remoteEP.Address, DnsTransportProtocol.Tcp))\n                    {\n                        _statsManager.QueueUpdate(null, remoteEP, protocol, null, true);\n                        break;\n                    }\n\n                    DnsDatagram request;\n\n                    //read dns datagram with timeout\n                    using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())\n                    {\n                        Task<DnsDatagram> task = DnsDatagram.ReadFromTcpAsync(stream, readBuffer, cancellationTokenSource.Token);\n\n                        if ((await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) && (task.Status != TaskStatus.RanToCompletion))\n                        {\n                            //read timed out\n                            await stream.DisposeAsync();\n                            return;\n                        }\n\n                        cancellationTokenSource.Cancel(); //cancel delay task\n\n                        request = await task;\n                        request.SetMetadata(dnsEP);\n                    }\n\n                    //process request async\n                    _ = ProcessStreamRequestAsync(stream, writeBuffer, writeSemaphore, remoteEP, request, protocol);\n                }\n            }\n            catch (ObjectDisposedException)\n            {\n                //ignore\n            }\n            catch (IOException)\n            {\n                //ignore IO exceptions\n            }\n            catch (Exception ex)\n            {\n                _log.Write(remoteEP, protocol, ex);\n            }\n        }\n\n        private async Task ProcessStreamRequestAsync(Stream stream, MemoryStream writeBuffer, SemaphoreSlim writeSemaphore, IPEndPoint remoteEP, DnsDatagram request, DnsTransportProtocol protocol)\n        {\n            try\n            {\n                DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address));\n                if (response is null)\n                {\n                    await stream.DisposeAsync();\n\n                    _statsManager.QueueUpdate(null, remoteEP, protocol, null, false);\n                    return; //drop request\n                }\n\n                //send response\n                await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1)\n                {\n                    await writeSemaphore.WaitAsync(cancellationToken1);\n                    try\n                    {\n                        //send dns datagram\n                        await response.WriteToTcpAsync(stream, writeBuffer, cancellationToken1);\n                        await stream.FlushAsync(cancellationToken1);\n                    }\n                    finally\n                    {\n                        writeSemaphore.Release();\n                    }\n                }, _tcpSendTimeout);\n\n                _queryLog?.Write(remoteEP, protocol, request, response);\n                _statsManager.QueueUpdate(request, remoteEP, protocol, response, false);\n            }\n            catch (ObjectDisposedException)\n            {\n                //ignore\n            }\n            catch (IOException)\n            {\n                //ignore IO exceptions\n            }\n            catch (Exception ex)\n            {\n                if (request is not null)\n                    _queryLog?.Write(remoteEP, protocol, request, null);\n\n                _log.Write(remoteEP, protocol, ex);\n            }\n        }\n\n        private async Task AcceptQuicConnectionAsync(QuicListener quicListener)\n        {\n            try\n            {\n                while (true)\n                {\n                    try\n                    {\n                        QuicConnection quicConnection = await quicListener.AcceptConnectionAsync();\n\n                        _ = ProcessQuicConnectionAsync(quicConnection);\n                    }\n                    catch (AuthenticationException)\n                    {\n                        //ignore failed connection handshake\n                    }\n                    catch (QuicException ex)\n                    {\n                        if (ex.InnerException is OperationCanceledException)\n                            continue;\n\n                        throw;\n                    }\n                }\n            }\n            catch (ObjectDisposedException)\n            {\n                //server stopped\n            }\n            catch (Exception ex)\n            {\n                if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped))\n                    return; //server stopping\n\n                _log.Write(quicListener.LocalEndPoint, DnsTransportProtocol.Quic, ex);\n            }\n        }\n\n        private async Task ProcessQuicConnectionAsync(QuicConnection quicConnection)\n        {\n            try\n            {\n                NameServerAddress dnsEP;\n\n                if (string.IsNullOrEmpty(quicConnection.TargetHostName))\n                    dnsEP = new NameServerAddress(quicConnection.LocalEndPoint, DnsTransportProtocol.Quic);\n                else\n                    dnsEP = new NameServerAddress(quicConnection.TargetHostName, quicConnection.LocalEndPoint, DnsTransportProtocol.Quic);\n\n                while (true)\n                {\n                    if (HasQpmLimitExceeded(quicConnection.RemoteEndPoint.Address, DnsTransportProtocol.Tcp))\n                    {\n                        _statsManager.QueueUpdate(null, quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, null, true);\n                        break;\n                    }\n\n                    QuicStream quicStream = await quicConnection.AcceptInboundStreamAsync();\n\n                    _ = ProcessQuicStreamRequestAsync(quicStream, quicConnection.RemoteEndPoint, dnsEP);\n                }\n            }\n            catch (QuicException ex)\n            {\n                switch (ex.QuicError)\n                {\n                    case QuicError.ConnectionIdle:\n                    case QuicError.ConnectionAborted:\n                    case QuicError.ConnectionTimeout:\n                        break;\n\n                    default:\n                        _log.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex);\n                        break;\n                }\n            }\n            catch (Exception ex)\n            {\n                _log.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex);\n            }\n            finally\n            {\n                await quicConnection.DisposeAsync();\n            }\n        }\n\n        private async Task ProcessQuicStreamRequestAsync(QuicStream quicStream, IPEndPoint remoteEP, NameServerAddress dnsEP)\n        {\n            MemoryStream sharedBuffer = new MemoryStream(512);\n            DnsDatagram request = null;\n\n            try\n            {\n                //read dns datagram with timeout\n                using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())\n                {\n                    Task<DnsDatagram> task = DnsDatagram.ReadFromTcpAsync(quicStream, sharedBuffer, cancellationTokenSource.Token);\n\n                    if ((await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) && (task.Status != TaskStatus.RanToCompletion))\n                    {\n                        //read timed out\n                        quicStream.Abort(QuicAbortDirection.Both, (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR);\n                        return;\n                    }\n\n                    cancellationTokenSource.Cancel(); //cancel delay task\n\n                    request = await task;\n                    request.SetMetadata(dnsEP);\n                }\n\n                //process request async\n                DnsDatagram response = await ProcessRequestAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address));\n                if (response is null)\n                {\n                    _statsManager.QueueUpdate(null, remoteEP, DnsTransportProtocol.Quic, null, false);\n                    return; //drop request\n                }\n\n                //send response\n                await response.WriteToTcpAsync(quicStream, sharedBuffer);\n\n                _queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, response);\n                _statsManager.QueueUpdate(request, remoteEP, DnsTransportProtocol.Quic, response, false);\n            }\n            catch (IOException)\n            {\n                //ignore QuicException / IOException\n            }\n            catch (Exception ex)\n            {\n                if (request is not null)\n                    _queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, null);\n\n                _log.Write(remoteEP, DnsTransportProtocol.Quic, ex);\n            }\n            finally\n            {\n                await sharedBuffer.DisposeAsync();\n                await quicStream.DisposeAsync();\n            }\n        }\n\n        private async Task ProcessDoHRequestAsync(HttpContext context)\n        {\n            IPEndPoint remoteEP = context.GetRemoteEndPoint(); //get the socket connection remote EP\n            DnsDatagram dnsRequest = null;\n\n            try\n            {\n                HttpRequest request = context.Request;\n                HttpResponse response = context.Response;\n\n                if (NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL))\n                {\n                    //try to get client's actual IP from X-Real-IP header, if any\n                    if (!string.IsNullOrEmpty(_dnsOverHttpRealIpHeader))\n                    {\n                        string xRealIp = context.Request.Headers[_dnsOverHttpRealIpHeader];\n                        if (IPAddress.TryParse(xRealIp, out IPAddress address))\n                            remoteEP = new IPEndPoint(address, 0);\n                    }\n                }\n                else\n                {\n                    if (!request.IsHttps)\n                    {\n                        //DNS-over-HTTP insecure protocol is intended to be used with an SSL terminated reverse proxy like nginx on private network\n                        response.StatusCode = 403;\n                        await response.WriteAsync(\"DNS-over-HTTPS (DoH) queries are supported only on HTTPS.\");\n                        return;\n                    }\n                }\n\n                if (HasQpmLimitExceeded(remoteEP.Address, DnsTransportProtocol.Tcp))\n                {\n                    _statsManager.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, true);\n\n                    response.StatusCode = 429;\n                    await response.WriteAsync(\"Too Many Requests\");\n                    return;\n                }\n\n                switch (request.Method)\n                {\n                    case \"GET\":\n                        bool acceptsDoH = false;\n\n                        string requestAccept = request.Headers.Accept;\n                        if (string.IsNullOrEmpty(requestAccept))\n                        {\n                            acceptsDoH = true;\n                        }\n                        else\n                        {\n                            foreach (string mediaType in requestAccept.Split(','))\n                            {\n                                if (mediaType.Equals(\"application/dns-message\", StringComparison.OrdinalIgnoreCase))\n                                {\n                                    acceptsDoH = true;\n                                    break;\n                                }\n                            }\n                        }\n\n                        if (!acceptsDoH)\n                        {\n                            response.Redirect((request.IsHttps ? \"https://\" : \"http://\") + request.Headers.Host);\n                            return;\n                        }\n\n                        string dnsRequestBase64Url = request.Query[\"dns\"];\n                        if (string.IsNullOrEmpty(dnsRequestBase64Url))\n                        {\n                            response.StatusCode = 400;\n                            await response.WriteAsync(\"Bad Request\");\n                            return;\n                        }\n\n                        //convert from base64url to base64\n                        dnsRequestBase64Url = dnsRequestBase64Url.Replace('-', '+');\n                        dnsRequestBase64Url = dnsRequestBase64Url.Replace('_', '/');\n\n                        //add padding\n                        int x = dnsRequestBase64Url.Length % 4;\n                        if (x > 0)\n                            dnsRequestBase64Url = dnsRequestBase64Url.PadRight(dnsRequestBase64Url.Length - x + 4, '=');\n\n                        using (MemoryStream mS = new MemoryStream(Convert.FromBase64String(dnsRequestBase64Url)))\n                        {\n                            dnsRequest = DnsDatagram.ReadFrom(mS);\n                            dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress()));\n                        }\n\n                        break;\n\n                    case \"POST\":\n                        if (!string.Equals(request.Headers.ContentType, \"application/dns-message\", StringComparison.OrdinalIgnoreCase))\n                        {\n                            response.StatusCode = 415;\n                            await response.WriteAsync(\"Unsupported Media Type\");\n                            return;\n                        }\n\n                        using (MemoryStream mS = new MemoryStream(32))\n                        {\n                            await request.Body.CopyToAsync(mS, 32);\n\n                            mS.Position = 0;\n                            dnsRequest = DnsDatagram.ReadFrom(mS);\n                            dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress()));\n                        }\n\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n\n                DnsDatagram dnsResponse = await ProcessRequestAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address));\n                if (dnsResponse is null)\n                {\n                    //drop request\n                    context.Connection.RequestClose();\n\n                    _statsManager.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, false);\n                    return;\n                }\n\n                using (MemoryStream mS = new MemoryStream(512))\n                {\n                    dnsResponse.WriteTo(mS);\n\n                    mS.Position = 0;\n                    response.ContentType = \"application/dns-message\";\n                    response.ContentLength = mS.Length;\n\n                    await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1)\n                    {\n                        await using (Stream s = response.Body)\n                        {\n                            await mS.CopyToAsync(s, 512, cancellationToken1);\n                        }\n                    }, _tcpSendTimeout);\n                }\n\n                _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, dnsResponse);\n                _statsManager.QueueUpdate(dnsRequest, remoteEP, DnsTransportProtocol.Https, dnsResponse, false);\n            }\n            catch (IOException)\n            {\n                //ignore IO exceptions\n            }\n            catch (Exception ex)\n            {\n                if (dnsRequest is not null)\n                    _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, null);\n\n                _log.Write(remoteEP, DnsTransportProtocol.Https, ex);\n            }\n        }\n\n        private bool IsRecursionAllowed(IPAddress remoteIP)\n        {\n            switch (_recursion)\n            {\n                case DnsServerRecursion.Allow:\n                    return true;\n\n                case DnsServerRecursion.AllowOnlyForPrivateNetworks:\n                    switch (remoteIP.AddressFamily)\n                    {\n                        case AddressFamily.InterNetwork:\n                        case AddressFamily.InterNetworkV6:\n                            return NetUtilities.IsPrivateIP(remoteIP);\n\n                        default:\n                            return false;\n                    }\n\n                case DnsServerRecursion.UseSpecifiedNetworkACL:\n                    return NetworkAccessControl.IsAddressAllowed(remoteIP, _recursionNetworkACL, true);\n\n                default:\n                    return false;\n            }\n        }\n\n        private async Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)\n        {\n            foreach (IDnsRequestController requestController in _dnsApplicationManager.DnsRequestControllers)\n            {\n                try\n                {\n                    DnsRequestControllerAction action = await requestController.GetRequestActionAsync(request, remoteEP, protocol);\n                    switch (action)\n                    {\n                        case DnsRequestControllerAction.DropSilently:\n                            return null; //drop request\n\n                        case DnsRequestControllerAction.DropWithRefused:\n                            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\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(remoteEP, protocol, ex);\n                }\n            }\n\n            if (request.ParsingException is not null)\n            {\n                //format error\n                if (request.ParsingException is not IOException)\n                    _log.Write(remoteEP, protocol, request.ParsingException);\n\n                //format error response\n                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 };\n            }\n\n            if (request.IsSigned)\n            {\n                if (!request.VerifySignedRequest(_tsigKeys, out DnsDatagram unsignedRequest, out DnsDatagram errorResponse))\n                {\n                    _log.Write(remoteEP, protocol, \"DNS Server received a request that failed TSIG signature verification (RCODE: \" + errorResponse.RCODE + \"; TSIG Error: \" + errorResponse.TsigError + \")\");\n\n                    errorResponse.Tag = DnsServerResponseType.Authoritative;\n                    return errorResponse;\n                }\n\n                DnsDatagram unsignedResponse = await ProcessQueryAsync(unsignedRequest, remoteEP, protocol, isRecursionAllowed, false, _clientTimeout, request.TsigKeyName);\n                if (unsignedResponse is null)\n                    return null;\n\n                unsignedResponse = await PostProcessQueryAsync(request, remoteEP, protocol, unsignedResponse);\n                if (unsignedResponse is null)\n                    return null;\n\n                return unsignedResponse.SignResponse(request, _tsigKeys);\n            }\n\n            if (request.EDNS is not null)\n            {\n                if (request.EDNS.Version != 0)\n                    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 };\n            }\n\n            DnsDatagram response = await ProcessQueryAsync(request, remoteEP, protocol, isRecursionAllowed, false, _clientTimeout, null);\n            if (response is null)\n                return null;\n\n            return await PostProcessQueryAsync(request, remoteEP, protocol, response);\n        }\n\n        private async Task<DnsDatagram> PostProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)\n        {\n            foreach (IDnsPostProcessor postProcessor in _dnsApplicationManager.DnsPostProcessors)\n            {\n                try\n                {\n                    response = await postProcessor.PostProcessAsync(request, remoteEP, protocol, response);\n                    if (response is null)\n                        return null; //drop request\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(remoteEP, protocol, ex);\n                }\n            }\n\n            if (request.EDNS is null)\n            {\n                if (response.EDNS is not null)\n                    response = response.CloneWithoutEDns();\n\n                return response;\n            }\n\n            if (response.EDNS is not null)\n                return response;\n\n            IReadOnlyList<EDnsOption> options = null;\n\n            EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true);\n            if (requestECS is not null)\n                options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address);\n\n            if (response.Additional.Count == 0)\n                return response.Clone(null, null, new DnsResourceRecord[] { DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options) });\n\n            if (response.IsSigned)\n                return response;\n\n            DnsResourceRecord[] newAdditional = new DnsResourceRecord[response.Additional.Count + 1];\n\n            for (int i = 0; i < response.Additional.Count; i++)\n                newAdditional[i] = response.Additional[i];\n\n            newAdditional[response.Additional.Count] = DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options);\n\n            return response.Clone(null, null, newAdditional);\n        }\n\n        private async Task<DnsDatagram> ProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout, string tsigAuthenticatedKeyName)\n        {\n            if (request.IsResponse)\n                return null; //drop response datagram to avoid loops in rare scenarios\n\n            switch (request.OPCODE)\n            {\n                case DnsOpcode.StandardQuery:\n                    if (request.Question.Count != 1)\n                        return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                    if (request.Question[0].Class != DnsClass.IN)\n                        return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                    try\n                    {\n                        DnsQuestionRecord question = request.Question[0];\n\n                        switch (question.Type)\n                        {\n                            case DnsResourceRecordType.AXFR:\n                                if (protocol == DnsTransportProtocol.Udp)\n                                    return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName);\n\n                            case DnsResourceRecordType.IXFR:\n                                return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName);\n\n                            case DnsResourceRecordType.FWD:\n                            case DnsResourceRecordType.APP:\n                                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                        }\n\n                        //query authoritative zone\n                        DnsDatagram response = await ProcessAuthoritativeQueryAsync(request, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers);\n                        if (response is not null)\n                        {\n                            if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request\n                                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, response.RCODE, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                            return response;\n                        }\n\n                        if (!request.RecursionDesired || !isRecursionAllowed)\n                            return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                        //do recursive query\n                        if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request\n                            return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                        return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                    }\n                    catch (InvalidDomainNameException)\n                    {\n                        //format error response\n                        return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                    }\n                    catch (TimeoutException ex)\n                    {\n                        DnsDatagram response = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                        _log.Write(remoteEP, protocol, request, response);\n                        _log.Write(remoteEP, protocol, ex);\n\n                        return response;\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(remoteEP, protocol, ex);\n\n                        return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                    }\n\n                case DnsOpcode.Notify:\n                    return await ProcessNotifyQueryAsync(request, remoteEP, protocol);\n\n                case DnsOpcode.Update:\n                    return await ProcessUpdateQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName);\n\n                default:\n                    return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NotImplemented, request.Question) { Tag = DnsServerResponseType.Authoritative };\n            }\n        }\n\n        private async Task<DnsDatagram> ProcessNotifyQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol)\n        {\n            AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name);\n            if ((zoneInfo is null) || ((zoneInfo.Type != AuthZoneType.Secondary) && (zoneInfo.Type != AuthZoneType.SecondaryForwarder) && (zoneInfo.Type != AuthZoneType.SecondaryCatalog)) || zoneInfo.Disabled)\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n            async Task<bool> RemoteVerifiedAsync(IPAddress remoteAddress)\n            {\n                if (_notifyAllowedNetworks is not null)\n                {\n                    foreach (NetworkAddress notifyAllowedNetwork in _notifyAllowedNetworks)\n                    {\n                        if (notifyAllowedNetwork.Contains(remoteAddress))\n                            return true;\n                    }\n                }\n\n                IReadOnlyList<NameServerAddress> primaryNameServerAddresses;\n\n                SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone;\n\n                if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers)\n                    primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses);\n                else\n                    primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync();\n\n                foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses)\n                {\n                    if (primaryNameServer.IPEndPoint.Address.Equals(remoteAddress))\n                        return true;\n                }\n\n                return false;\n            }\n\n            if (!await RemoteVerifiedAsync(remoteEP.Address))\n            {\n                _log.Write(remoteEP, protocol, \"DNS Server refused a NOTIFY request since the request IP address was not recognized by the secondary zone: \" + zoneInfo.DisplayName);\n\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n            }\n\n            _log.Write(remoteEP, protocol, \"DNS Server received a NOTIFY request for secondary zone: \" + zoneInfo.DisplayName);\n\n            if ((request.Answer.Count > 0) && (request.Answer[0].Type == DnsResourceRecordType.SOA))\n            {\n                IReadOnlyList<DnsResourceRecord> localSoaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);\n\n                if (!DnsSOARecordData.IsZoneUpdateAvailable((localSoaRecords[0].RDATA as DnsSOARecordData).Serial, (request.Answer[0].RDATA as DnsSOARecordData).Serial))\n                {\n                    //no update was available\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                }\n            }\n\n            zoneInfo.TriggerRefresh();\n            return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n        }\n\n        private async Task<DnsDatagram> ProcessUpdateQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName)\n        {\n            if ((request.Question.Count != 1) || (request.Question[0].Type != DnsResourceRecordType.SOA))\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n            if (request.Question[0].Class != DnsClass.IN)\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n            AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name);\n            if ((zoneInfo is null) || zoneInfo.Disabled)\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n            _log.Write(remoteEP, protocol, \"DNS Server received a zone UPDATE request for zone: \" + zoneInfo.DisplayName);\n\n            async Task<bool> IsZoneNameServerAllowedAsync()\n            {\n                IPAddress remoteAddress = remoteEP.Address;\n                IReadOnlyList<NameServerAddress> secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync();\n\n                foreach (NameServerAddress secondaryNameServer in secondaryNameServers)\n                {\n                    if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress))\n                        return true;\n                }\n\n                return false;\n            }\n\n            async Task<bool> IsUpdatePermittedAsync()\n            {\n                bool isUpdateAllowed;\n\n                switch (zoneInfo.Update)\n                {\n                    case AuthZoneUpdate.Allow:\n                        isUpdateAllowed = true;\n                        break;\n\n                    case AuthZoneUpdate.AllowOnlyZoneNameServers:\n                        isUpdateAllowed = await IsZoneNameServerAllowedAsync();\n                        break;\n\n                    case AuthZoneUpdate.UseSpecifiedNetworkACL:\n                        isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL);\n                        break;\n\n                    case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL) || await IsZoneNameServerAllowedAsync();\n                        break;\n\n                    case AuthZoneUpdate.Deny:\n                    default:\n                        isUpdateAllowed = false;\n                        break;\n                }\n\n                if (!isUpdateAllowed)\n                {\n                    _log.Write(remoteEP, protocol, \"DNS Server refused a zone UPDATE request since the request IP address is not allowed by the zone: \" + zoneInfo.DisplayName);\n\n                    return false;\n                }\n\n                //check security policies\n                if ((zoneInfo.UpdateSecurityPolicies is not null) && (zoneInfo.UpdateSecurityPolicies.Count > 0))\n                {\n                    if ((tsigAuthenticatedKeyName is null) || !zoneInfo.UpdateSecurityPolicies.TryGetValue(tsigAuthenticatedKeyName.ToLowerInvariant(), out IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>> policyMap))\n                    {\n                        _log.Write(remoteEP, protocol, \"DNS Server refused a zone UPDATE request since the request is missing TSIG auth required by the zone: \" + zoneInfo.DisplayName);\n\n                        return false;\n                    }\n\n                    //check policy\n                    foreach (DnsResourceRecord uRecord in request.Authority)\n                    {\n                        bool isPermitted = false;\n\n                        foreach (KeyValuePair<string, IReadOnlyList<DnsResourceRecordType>> policy in policyMap)\n                        {\n                            if (\n                                  uRecord.Name.Equals(policy.Key, StringComparison.OrdinalIgnoreCase) ||\n                                  (policy.Key.StartsWith(\"*.\") && uRecord.Name.EndsWith(policy.Key.Substring(1), StringComparison.OrdinalIgnoreCase))\n                               )\n                            {\n                                foreach (DnsResourceRecordType allowedType in policy.Value)\n                                {\n                                    if ((allowedType == DnsResourceRecordType.ANY) || (allowedType == uRecord.Type))\n                                    {\n                                        isPermitted = true;\n                                        break;\n                                    }\n                                }\n\n                                if (isPermitted)\n                                    break;\n                            }\n                        }\n\n                        if (!isPermitted)\n                        {\n                            _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);\n\n                            return false;\n                        }\n                    }\n                }\n\n                return true;\n            }\n\n            switch (zoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                case AuthZoneType.Forwarder:\n                    //update\n                    {\n                        //process prerequisite section\n                        {\n                            Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> temp = new Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>>();\n\n                            foreach (DnsResourceRecord prRecord in request.Answer)\n                            {\n                                if (prRecord.TTL != 0)\n                                    return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(prRecord.Name);\n                                if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                                    return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                if (prRecord.Class == DnsClass.ANY)\n                                {\n                                    if (prRecord.RDATA.RDLENGTH != 0)\n                                        return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                    if (prRecord.Type == DnsResourceRecordType.ANY)\n                                    {\n                                        //check if name is in use\n                                        if (!_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name))\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                    else\n                                    {\n                                        //check if RRSet exists (value independent)\n                                        IReadOnlyList<DnsResourceRecord> rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type);\n                                        if (rrset.Count == 0)\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                }\n                                else if (prRecord.Class == DnsClass.NONE)\n                                {\n                                    if (prRecord.RDATA.RDLENGTH != 0)\n                                        return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                    if (prRecord.Type == DnsResourceRecordType.ANY)\n                                    {\n                                        //check if name is not in use\n                                        if (_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name))\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXDomain, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                    else\n                                    {\n                                        //check if RRSet does not exists\n                                        IReadOnlyList<DnsResourceRecord> rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type);\n                                        if (rrset.Count > 0)\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                }\n                                else if (prRecord.Class == request.Question[0].Class)\n                                {\n                                    //check if RRSet exists (value dependent)\n                                    //add to temp for later comparison\n                                    string recordName = prRecord.Name.ToLowerInvariant();\n\n                                    if (!temp.TryGetValue(recordName, out Dictionary<DnsResourceRecordType, List<DnsResourceRecord>> rrsetEntry))\n                                    {\n                                        rrsetEntry = new Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>();\n                                        temp.Add(recordName, rrsetEntry);\n                                    }\n\n                                    if (!rrsetEntry.TryGetValue(prRecord.Type, out List<DnsResourceRecord> rrset))\n                                    {\n                                        rrset = new List<DnsResourceRecord>();\n                                        rrsetEntry.Add(prRecord.Type, rrset);\n                                    }\n\n                                    rrset.Add(prRecord);\n                                }\n                                else\n                                {\n                                    //FORMERR\n                                    return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                }\n                            }\n\n                            //compare collected RRSets in temp\n                            foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> zoneEntry in temp)\n                            {\n                                foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> rrsetEntry in zoneEntry.Value)\n                                {\n                                    List<DnsResourceRecord> prRRSet = rrsetEntry.Value;\n                                    IReadOnlyList<DnsResourceRecord> rrset = _authZoneManager.GetRecords(zoneInfo.Name, zoneEntry.Key, rrsetEntry.Key);\n\n                                    //check if RRSet exists (value dependent)\n                                    //compare RRSets\n\n                                    if (prRRSet.Count != rrset.Count)\n                                        return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                    foreach (DnsResourceRecord prRecord in prRRSet)\n                                    {\n                                        bool found = false;\n\n                                        foreach (DnsResourceRecord record in rrset)\n                                        {\n                                            if (\n                                                prRecord.Name.Equals(record.Name, StringComparison.OrdinalIgnoreCase) &&\n                                                (prRecord.Class == record.Class) &&\n                                                (prRecord.Type == record.Type) &&\n                                                (prRecord.RDATA.RDLENGTH == record.RDATA.RDLENGTH) &&\n                                                prRecord.RDATA.Equals(record.RDATA)\n                                               )\n                                            {\n                                                found = true;\n                                                break;\n                                            }\n                                        }\n\n                                        if (!found)\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                }\n                            }\n                        }\n\n                        //check for permissions\n                        if (!await IsUpdatePermittedAsync())\n                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                        //process update section\n                        {\n                            //prescan\n                            foreach (DnsResourceRecord uRecord in request.Authority)\n                            {\n                                AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(uRecord.Name);\n                                if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                                    return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                if (uRecord.Class == request.Question[0].Class)\n                                {\n                                    if (uRecord.RDATA.RDLENGTH == 0) //RDATA must be present to add record\n                                        return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                    switch (uRecord.Type)\n                                    {\n                                        case DnsResourceRecordType.ANY:\n                                        case DnsResourceRecordType.AXFR:\n                                        case DnsResourceRecordType.MAILA:\n                                        case DnsResourceRecordType.MAILB:\n                                        case DnsResourceRecordType.IXFR:\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                }\n                                else if (uRecord.Class == DnsClass.ANY)\n                                {\n                                    if ((uRecord.TTL != 0) || (uRecord.RDATA.RDLENGTH != 0))\n                                        return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                    switch (uRecord.Type)\n                                    {\n                                        case DnsResourceRecordType.AXFR:\n                                        case DnsResourceRecordType.MAILA:\n                                        case DnsResourceRecordType.MAILB:\n                                        case DnsResourceRecordType.IXFR:\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                }\n                                else if (uRecord.Class == DnsClass.NONE)\n                                {\n                                    if ((uRecord.TTL != 0) || (uRecord.RDATA.RDLENGTH == 0)) //RDATA must be present for deletion\n                                        return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                                    switch (uRecord.Type)\n                                    {\n                                        case DnsResourceRecordType.ANY:\n                                        case DnsResourceRecordType.AXFR:\n                                        case DnsResourceRecordType.MAILA:\n                                        case DnsResourceRecordType.MAILB:\n                                        case DnsResourceRecordType.IXFR:\n                                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                    }\n                                }\n                                else\n                                {\n                                    //FORMERR\n                                    return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                                }\n                            }\n\n                            //update\n                            Dictionary<string, Dictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> originalRRSets = new Dictionary<string, Dictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>>();\n\n                            void AddToOriginalRRSets(string domain, DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> existingRRSet)\n                            {\n                                if (!originalRRSets.TryGetValue(domain, out Dictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> originalRRSetEntries))\n                                {\n                                    originalRRSetEntries = new Dictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>();\n                                    originalRRSets.Add(domain, originalRRSetEntries);\n                                }\n\n                                originalRRSetEntries.TryAdd(type, existingRRSet);\n                            }\n\n                            try\n                            {\n                                foreach (DnsResourceRecord uRecord in request.Authority)\n                                {\n                                    if (uRecord.Class == request.Question[0].Class)\n                                    {\n                                        //Add to an RRset\n                                        if (uRecord.Type == DnsResourceRecordType.CNAME)\n                                        {\n                                            if (_authZoneManager.NameExists(zoneInfo.Name, uRecord.Name) && (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count == 0))\n                                                continue; //current name exists and has non-CNAME records so cannot add CNAME record\n\n                                            IReadOnlyList<DnsResourceRecord> existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);\n                                            AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);\n\n                                            GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo();\n                                            recordInfo.LastModified = DateTime.UtcNow;\n                                            recordInfo.Comments = \"Via Dynamic Updates (RFC 2136)\" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? \"\" : \" using key '\" + tsigAuthenticatedKeyName + \"'\") + \" from '\" + remoteEP.ToString() + \"'\";\n\n                                            _authZoneManager.SetRecord(zoneInfo.Name, uRecord);\n                                        }\n                                        else if (uRecord.Type == DnsResourceRecordType.DNAME)\n                                        {\n                                            IReadOnlyList<DnsResourceRecord> existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);\n                                            AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);\n\n                                            GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo();\n                                            recordInfo.LastModified = DateTime.UtcNow;\n                                            recordInfo.Comments = \"Via Dynamic Updates (RFC 2136)\" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? \"\" : \" using key '\" + tsigAuthenticatedKeyName + \"'\") + \" from '\" + remoteEP.ToString() + \"'\";\n\n                                            _authZoneManager.SetRecord(zoneInfo.Name, uRecord);\n                                        }\n                                        else if (uRecord.Type == DnsResourceRecordType.SOA)\n                                        {\n                                            if (!uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                                                continue; //can add SOA only to apex\n\n                                            IReadOnlyList<DnsResourceRecord> existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);\n                                            AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);\n\n                                            GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo();\n                                            recordInfo.LastModified = DateTime.UtcNow;\n                                            recordInfo.Comments = \"Via Dynamic Updates (RFC 2136)\" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? \"\" : \" using key '\" + tsigAuthenticatedKeyName + \"'\") + \" from '\" + remoteEP.ToString() + \"'\";\n\n                                            _authZoneManager.SetRecord(zoneInfo.Name, uRecord);\n                                        }\n                                        else\n                                        {\n                                            if (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count > 0)\n                                                continue; //current name contains CNAME so cannot add non-CNAME record\n\n                                            IReadOnlyList<DnsResourceRecord> existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);\n                                            AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);\n\n                                            if (uRecord.Type == DnsResourceRecordType.NS)\n                                                uRecord.SyncGlueRecords(request.Additional);\n\n                                            GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo();\n                                            recordInfo.LastModified = DateTime.UtcNow;\n                                            recordInfo.Comments = \"Via Dynamic Updates (RFC 2136)\" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? \"\" : \" using key '\" + tsigAuthenticatedKeyName + \"'\") + \" from '\" + remoteEP.ToString() + \"'\";\n\n                                            _authZoneManager.AddRecord(zoneInfo.Name, uRecord);\n                                        }\n                                    }\n                                    else if (uRecord.Class == DnsClass.ANY)\n                                    {\n                                        if (uRecord.Type == DnsResourceRecordType.ANY)\n                                        {\n                                            //Delete all RRsets from a name\n                                            IReadOnlyDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> existingRRSets = _authZoneManager.GetEntriesFor(zoneInfo.Name, uRecord.Name);\n\n                                            if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                                            {\n                                                foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> existingRRSet in existingRRSets)\n                                                {\n                                                    switch (existingRRSet.Key)\n                                                    {\n                                                        case DnsResourceRecordType.SOA:\n                                                        case DnsResourceRecordType.NS:\n                                                        case DnsResourceRecordType.DNSKEY:\n                                                        case DnsResourceRecordType.RRSIG:\n                                                        case DnsResourceRecordType.NSEC:\n                                                        case DnsResourceRecordType.NSEC3PARAM:\n                                                        case DnsResourceRecordType.NSEC3:\n                                                            continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets\n                                                    }\n\n                                                    AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value);\n\n                                                    _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key);\n                                                }\n                                            }\n                                            else\n                                            {\n                                                foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> existingRRSet in existingRRSets)\n                                                {\n                                                    switch (existingRRSet.Key)\n                                                    {\n                                                        case DnsResourceRecordType.DNSKEY:\n                                                        case DnsResourceRecordType.RRSIG:\n                                                        case DnsResourceRecordType.NSEC:\n                                                        case DnsResourceRecordType.NSEC3PARAM:\n                                                        case DnsResourceRecordType.NSEC3:\n                                                            continue; //skip DNSSEC rrsets\n                                                    }\n\n                                                    AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value);\n\n                                                    _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key);\n                                                }\n                                            }\n                                        }\n                                        else\n                                        {\n                                            //Delete an RRset\n                                            if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                                            {\n                                                switch (uRecord.Type)\n                                                {\n                                                    case DnsResourceRecordType.SOA:\n                                                    case DnsResourceRecordType.NS:\n                                                    case DnsResourceRecordType.DNSKEY:\n                                                    case DnsResourceRecordType.RRSIG:\n                                                    case DnsResourceRecordType.NSEC:\n                                                    case DnsResourceRecordType.NSEC3PARAM:\n                                                    case DnsResourceRecordType.NSEC3:\n                                                        continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets\n                                                }\n                                            }\n\n                                            IReadOnlyList<DnsResourceRecord> existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);\n                                            AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);\n\n                                            _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);\n                                        }\n                                    }\n                                    else if (uRecord.Class == DnsClass.NONE)\n                                    {\n                                        //Delete an RR from an RRset\n\n                                        switch (uRecord.Type)\n                                        {\n                                            case DnsResourceRecordType.SOA:\n                                            case DnsResourceRecordType.DNSKEY:\n                                            case DnsResourceRecordType.RRSIG:\n                                            case DnsResourceRecordType.NSEC:\n                                            case DnsResourceRecordType.NSEC3PARAM:\n                                            case DnsResourceRecordType.NSEC3:\n                                                continue; //no SOA can be deleted; skip DNSSEC rrsets\n                                        }\n\n                                        IReadOnlyList<DnsResourceRecord> existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type);\n\n                                        if ((uRecord.Type == DnsResourceRecordType.NS) && (existingRRSet.Count == 1) && uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                                            continue; //no apex NS can be deleted if only 1 NS exists\n\n                                        AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet);\n\n                                        _authZoneManager.DeleteRecord(zoneInfo.Name, uRecord.Name, uRecord.Type, uRecord.RDATA);\n                                    }\n                                }\n                            }\n                            catch\n                            {\n                                //revert\n                                foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> originalRRSetEntries in originalRRSets)\n                                {\n                                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> originalRRSet in originalRRSetEntries.Value)\n                                    {\n                                        if (originalRRSet.Value.Count == 0)\n                                            _authZoneManager.DeleteRecords(zoneInfo.Name, originalRRSetEntries.Key, originalRRSet.Key);\n                                        else\n                                            _authZoneManager.SetRecords(zoneInfo.Name, originalRRSet.Value);\n                                    }\n                                }\n\n                                throw;\n                            }\n                        }\n\n                        _authZoneManager.SaveZoneFile(zoneInfo.Name);\n\n                        _log.Write(remoteEP, protocol, \"DNS Server successfully processed a zone UPDATE request for zone: \" + zoneInfo.DisplayName);\n\n                        //NOERROR\n                        return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                    }\n\n                case AuthZoneType.Secondary:\n                case AuthZoneType.SecondaryForwarder:\n                    //forward\n                    {\n                        //check for permissions\n                        if (!await IsUpdatePermittedAsync())\n                            return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n                        //forward to primary\n                        IReadOnlyList<NameServerAddress> primaryNameServerAddresses;\n                        DnsTransportProtocol primaryZoneTransferProtocol;\n\n                        SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone;\n\n                        if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers)\n                        {\n                            primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses);\n                            primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol;\n                        }\n                        else\n                        {\n                            primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync();\n                            primaryZoneTransferProtocol = zoneInfo.PrimaryZoneTransferProtocol;\n                        }\n\n                        switch (primaryZoneTransferProtocol)\n                        {\n                            case DnsTransportProtocol.Tls:\n                            case DnsTransportProtocol.Quic:\n                                {\n                                    //change name server protocol to TLS/QUIC\n                                    List<NameServerAddress> updatedNameServers = new List<NameServerAddress>(primaryNameServerAddresses.Count);\n\n                                    foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses)\n                                    {\n                                        if (primaryNameServer.Protocol == primaryZoneTransferProtocol)\n                                            updatedNameServers.Add(primaryNameServer);\n                                        else\n                                            updatedNameServers.Add(primaryNameServer.Clone(primaryZoneTransferProtocol));\n                                    }\n\n                                    primaryNameServerAddresses = updatedNameServers;\n                                }\n                                break;\n\n                            default:\n                                if (protocol == DnsTransportProtocol.Tcp)\n                                {\n                                    //change name server protocol to TCP\n                                    List<NameServerAddress> updatedNameServers = new List<NameServerAddress>(primaryNameServerAddresses.Count);\n\n                                    foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses)\n                                    {\n                                        if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp)\n                                            updatedNameServers.Add(primaryNameServer);\n                                        else\n                                            updatedNameServers.Add(primaryNameServer.Clone(DnsTransportProtocol.Tcp));\n                                    }\n\n                                    primaryNameServerAddresses = updatedNameServers;\n                                }\n                                break;\n                        }\n\n                        TsigKey key = null;\n\n                        if (!string.IsNullOrEmpty(tsigAuthenticatedKeyName) && ((_tsigKeys is null) || !_tsigKeys.TryGetValue(tsigAuthenticatedKeyName, out key)))\n                            throw new DnsServerException(\"DNS Server does not have TSIG key '\" + tsigAuthenticatedKeyName + \"' configured to authenticate dynamic updates for \" + zoneInfo.TypeName + \" zone: \" + zoneInfo.DisplayName);\n\n                        DnsClient dnsClient = new DnsClient(primaryNameServerAddresses);\n\n                        dnsClient.Proxy = _proxy;\n                        dnsClient.PreferIPv6 = _preferIPv6;\n                        dnsClient.Retries = _forwarderRetries;\n                        dnsClient.Timeout = _forwarderTimeout;\n                        dnsClient.Concurrency = 1;\n\n                        DnsDatagram newRequest = request.Clone();\n                        newRequest.SetRandomIdentifier();\n\n                        DnsDatagram newResponse;\n\n                        if (key is null)\n                            newResponse = await dnsClient.RawResolveAsync(newRequest);\n                        else\n                            newResponse = await dnsClient.TsigResolveAsync(newRequest, key);\n\n                        newResponse.SetIdentifier(request.Identifier);\n\n                        return newResponse;\n                    }\n\n                default:\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative };\n            }\n        }\n\n        private async Task<DnsDatagram> ProcessZoneTransferQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName)\n        {\n            AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name);\n            if ((zoneInfo is null) || !zoneInfo.ApexZone.IsActive)\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n\n            switch (zoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                case AuthZoneType.Secondary:\n                case AuthZoneType.Forwarder:\n                case AuthZoneType.Catalog:\n                    break;\n\n                default:\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n            }\n\n            async Task<bool> IsZoneNameServerAllowedAsync(ApexZone apexZone)\n            {\n                IPAddress remoteAddress = remoteEP.Address;\n                IReadOnlyList<NameServerAddress> secondaryNameServers = await apexZone.GetResolvedSecondaryNameServerAddressesAsync();\n\n                foreach (NameServerAddress secondaryNameServer in secondaryNameServers)\n                {\n                    if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress))\n                        return true;\n                }\n\n                return false;\n            }\n\n            async Task<bool> IsZoneTransferAllowed(ApexZone apexZone)\n            {\n                switch (apexZone.ZoneTransfer)\n                {\n                    case AuthZoneTransfer.Allow:\n                        return true;\n\n                    case AuthZoneTransfer.AllowOnlyZoneNameServers:\n                        return await IsZoneNameServerAllowedAsync(apexZone);\n\n                    case AuthZoneTransfer.UseSpecifiedNetworkACL:\n                        return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL);\n\n                    case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL) || await IsZoneNameServerAllowedAsync(apexZone);\n\n                    case AuthZoneTransfer.Deny:\n                    default:\n                        return false;\n                }\n            }\n\n            bool IsTsigAuthenticated(ApexZone apexZone)\n            {\n                if ((apexZone.ZoneTransferTsigKeyNames is null) || (apexZone.ZoneTransferTsigKeyNames.Count < 1))\n                    return true; //no auth needed\n\n                if ((tsigAuthenticatedKeyName is not null) && apexZone.ZoneTransferTsigKeyNames.Contains(tsigAuthenticatedKeyName.ToLowerInvariant()))\n                    return true; //key matches\n\n                return false;\n            }\n\n            bool isInZoneTransferAllowedList = false;\n\n            if (_zoneTransferAllowedNetworks is not null)\n            {\n                IPAddress remoteAddress = remoteEP.Address;\n\n                foreach (NetworkAddress networkAddress in _zoneTransferAllowedNetworks)\n                {\n                    if (networkAddress.Contains(remoteAddress))\n                    {\n                        isInZoneTransferAllowedList = true;\n                        break;\n                    }\n                }\n            }\n\n            if (!isInZoneTransferAllowedList)\n            {\n                ApexZone apexZone = zoneInfo.ApexZone;\n\n                CatalogZone catalogZone = apexZone.CatalogZone;\n                if (catalogZone is not null)\n                {\n                    if (!apexZone.OverrideCatalogZoneTransfer)\n                        apexZone = catalogZone; //use catalog zone transfer options\n                }\n                else\n                {\n                    SecondaryCatalogZone secondaryCatalogZone = apexZone.SecondaryCatalogZone;\n                    if (secondaryCatalogZone is not null)\n                    {\n                        if (!apexZone.OverrideCatalogZoneTransfer)\n                            apexZone = secondaryCatalogZone; //use secondary zone transfer options\n                    }\n                }\n\n                if (!await IsZoneTransferAllowed(apexZone))\n                {\n                    _log.Write(remoteEP, protocol, \"DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: \" + zoneInfo.DisplayName);\n\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                }\n\n                if (!IsTsigAuthenticated(apexZone))\n                {\n                    _log.Write(remoteEP, protocol, \"DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: \" + zoneInfo.DisplayName);\n\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                }\n            }\n\n            _log.Write(remoteEP, protocol, \"DNS Server received zone transfer request for zone: \" + zoneInfo.DisplayName);\n\n            IReadOnlyList<DnsResourceRecord> xfrRecords;\n\n            if (request.Question[0].Type == DnsResourceRecordType.IXFR)\n            {\n                if ((request.Authority.Count == 1) && (request.Authority[0].Type == DnsResourceRecordType.SOA))\n                    xfrRecords = _authZoneManager.QueryIncrementalZoneTransferRecords(request.Question[0].Name, request.Authority[0]);\n                else\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n            }\n            else\n            {\n                xfrRecords = _authZoneManager.QueryZoneTransferRecords(request.Question[0].Name);\n            }\n\n            DnsDatagram xfrResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, xfrRecords) { Tag = DnsServerResponseType.Authoritative };\n            xfrResponse = xfrResponse.Split();\n\n            //update notify failed list\n            NameServerAddress allowedZoneNameServer = null;\n\n            switch (zoneInfo.Notify)\n            {\n                case AuthZoneNotify.ZoneNameServers:\n                case AuthZoneNotify.BothZoneAndSpecifiedNameServers:\n                    IPAddress remoteAddress = remoteEP.Address;\n                    IReadOnlyList<NameServerAddress> secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync();\n\n                    foreach (NameServerAddress secondaryNameServer in secondaryNameServers)\n                    {\n                        if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress))\n                        {\n                            allowedZoneNameServer = secondaryNameServer;\n                            break;\n                        }\n                    }\n\n                    break;\n            }\n\n            zoneInfo.ApexZone.RemoveFromNotifyFailedList(allowedZoneNameServer, remoteEP.Address);\n\n            return xfrResponse;\n        }\n\n        private async Task<DnsDatagram> ProcessAuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers)\n        {\n            DnsDatagram response = await AuthoritativeQueryAsync(request, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, remoteEP);\n            if (response is null)\n                return null;\n\n            bool reprocessResponse; //to allow resolving CNAME/ANAME in response\n            do\n            {\n                reprocessResponse = false;\n\n                if (response.RCODE == DnsResponseCode.NoError)\n                {\n                    if (response.Answer.Count > 0)\n                    {\n                        DnsResourceRecordType questionType = request.Question[0].Type;\n                        DnsResourceRecord lastRR = response.GetLastAnswerRecord();\n\n                        if ((lastRR.Type != questionType) && (questionType != DnsResourceRecordType.ANY))\n                        {\n                            switch (lastRR.Type)\n                            {\n                                case DnsResourceRecordType.CNAME:\n                                    return await ProcessCNAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout);\n\n                                case DnsResourceRecordType.ANAME:\n                                case DnsResourceRecordType.ALIAS:\n                                    return await ProcessANAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout);\n                            }\n                        }\n                    }\n                    else if (response.Authority.Count > 0)\n                    {\n                        DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord();\n                        switch (firstAuthority.Type)\n                        {\n                            case DnsResourceRecordType.NS:\n                                if (request.RecursionDesired && isRecursionAllowed)\n                                {\n                                    //do forced recursive resolution (with blocking support) using empty conditional forwarders; name servers will be provided via ResolverDnsCache\n                                    return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, [], _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout);\n                                }\n\n                                break;\n\n                            case DnsResourceRecordType.FWD:\n                                //do conditional forwarding (with blocking support)\n                                return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout);\n\n                            case DnsResourceRecordType.APP:\n                                response = await ProcessAPPAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout);\n                                if (response is null)\n                                    return null; //drop request\n\n                                reprocessResponse = true;\n                                break;\n                        }\n                    }\n                }\n            }\n            while (reprocessResponse);\n\n            return response;\n        }\n\n        internal async Task<DnsDatagram> AuthoritativeQueryAsync(DnsDatagram request, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, IPEndPoint remoteEP = null)\n        {\n            DnsDatagram authResponse;\n\n            if (remoteEP is null)\n                authResponse = _authZoneManager.Query(request, isRecursionAllowed);\n            else\n                authResponse = await _authZoneManager.QueryAsync(request, remoteEP.Address, isRecursionAllowed);\n\n            if (authResponse is not null)\n            {\n                if ((authResponse.RCODE != DnsResponseCode.NoError) || (authResponse.Answer.Count > 0) || (authResponse.Authority.Count == 0) || authResponse.IsFirstAuthoritySOA())\n                {\n                    authResponse.Tag = DnsServerResponseType.Authoritative;\n                    return authResponse;\n                }\n            }\n\n            DnsDatagram lastAppResponse = null;\n\n            if (!skipDnsAppAuthoritativeRequestHandlers)\n            {\n                if (remoteEP is null)\n                    remoteEP = IPENDPOINT_ANY_0;\n\n                foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsApplicationManager.DnsAuthoritativeRequestHandlers)\n                {\n                    try\n                    {\n                        DnsDatagram appResponse = await requestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed);\n                        if (appResponse is not null)\n                        {\n                            if ((appResponse.RCODE != DnsResponseCode.NoError) || (appResponse.Answer.Count > 0) || (appResponse.Authority.Count == 0) || appResponse.IsFirstAuthoritySOA())\n                            {\n                                if (appResponse.Tag is null)\n                                    appResponse.Tag = DnsServerResponseType.Authoritative;\n\n                                return appResponse;\n                            }\n\n                            if (lastAppResponse is null)\n                            {\n                                //keep last non-null app response to return later\n                                lastAppResponse = appResponse;\n                            }\n                            else\n                            {\n                                //compare last app response and current app response and select more specific response\n                                DnsResourceRecord appResponseFirstAuthority = appResponse.FindFirstAuthorityRecord();\n                                DnsResourceRecord lastAppResponseFirstAuthority = lastAppResponse.FindFirstAuthorityRecord();\n\n                                if (appResponseFirstAuthority.Name.Length > lastAppResponseFirstAuthority.Name.Length)\n                                    lastAppResponse = appResponse;\n                            }\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(remoteEP, protocol, ex);\n                    }\n                }\n            }\n\n            if ((authResponse is not null) && (authResponse.Authority.Count > 0))\n            {\n                if ((lastAppResponse is not null) && (lastAppResponse.Authority.Count > 0))\n                {\n                    DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord();\n                    DnsResourceRecord appResponseFirstAuthority = lastAppResponse.FindFirstAuthorityRecord();\n\n                    if (appResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length)\n                        return lastAppResponse;\n                }\n\n                return authResponse;\n            }\n            else\n            {\n                return lastAppResponse;\n            }\n        }\n\n        private async Task<DnsDatagram> ProcessAPPAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout)\n        {\n            DnsResourceRecord appResourceRecord = response.Authority[0];\n            DnsApplicationRecordData appRecord = appResourceRecord.RDATA as DnsApplicationRecordData;\n\n            if (_dnsApplicationManager.Applications.TryGetValue(appRecord.AppName, out DnsApplication application))\n            {\n                if (application.DnsAppRecordRequestHandlers.TryGetValue(appRecord.ClassPath, out IDnsAppRecordRequestHandler appRecordRequestHandler))\n                {\n                    AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(appResourceRecord.Name);\n\n                    DnsDatagram appResponse = await appRecordRequestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed, zoneInfo.Name, appResourceRecord.Name, appResourceRecord.TTL, appRecord.Data);\n                    if (appResponse is null)\n                    {\n                        DnsResponseCode rcode;\n                        IReadOnlyList<DnsResourceRecord> authority = null;\n\n                        if ((zoneInfo.Type == AuthZoneType.Forwarder) || (zoneInfo.Type == AuthZoneType.SecondaryForwarder))\n                        {\n                            //process FWD record if exists\n                            if (!zoneInfo.Name.Equals(appResourceRecord.Name, StringComparison.OrdinalIgnoreCase))\n                            {\n                                AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.Name, appResourceRecord.Name);\n                                if (authZone is not null)\n                                    authority = authZone.QueryRecords(DnsResourceRecordType.FWD, false);\n                            }\n\n                            if ((authority is null) || (authority.Count == 0))\n                                authority = zoneInfo.ApexZone.QueryRecords(DnsResourceRecordType.FWD, false);\n\n                            if (authority.Count > 0)\n                                return await RecursiveResolveAsync(request, remoteEP, authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n\n                            rcode = DnsResponseCode.NoError;\n                        }\n                        else\n                        {\n                            //return NODATA/NXDOMAIN response\n                            if ((request.Question[0].Name.Length == appResourceRecord.Name.Length) || appResourceRecord.Name.StartsWith('*'))\n                                rcode = DnsResponseCode.NoError;\n                            else\n                                rcode = DnsResponseCode.NxDomain;\n\n                            authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);\n                        }\n\n                        return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative };\n                    }\n                    else\n                    {\n                        if (appResponse.AuthoritativeAnswer)\n                            appResponse.Tag = DnsServerResponseType.Authoritative;\n\n                        return appResponse; //return app response\n                    }\n                }\n                else\n                {\n                    _log.Write(remoteEP, protocol, \"DNS request handler '\" + appRecord.ClassPath + \"' was not found in the application '\" + appRecord.AppName + \"': \" + appResourceRecord.Name);\n                }\n            }\n            else\n            {\n                _log.Write(remoteEP, protocol, \"DNS application '\" + appRecord.AppName + \"' was not found: \" + appResourceRecord.Name);\n            }\n\n            //return server failure response with SOA\n            {\n                AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name);\n                IReadOnlyList<DnsResourceRecord> authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);\n\n                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 };\n            }\n        }\n\n        private async Task<DnsDatagram> ProcessCNAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout)\n        {\n            List<DnsResourceRecord> newAnswer = new List<DnsResourceRecord>(response.Answer.Count + 4);\n            newAnswer.AddRange(response.Answer);\n\n            //copying NSEC/NSEC3 for for wildcard answers\n            List<DnsResourceRecord> newAuthority = new List<DnsResourceRecord>(2);\n\n            foreach (DnsResourceRecord record in response.Authority)\n            {\n                switch (record.Type)\n                {\n                    case DnsResourceRecordType.NSEC:\n                    case DnsResourceRecordType.NSEC3:\n                        newAuthority.Add(record);\n                        break;\n\n                    case DnsResourceRecordType.RRSIG:\n                        switch ((record.RDATA as DnsRRSIGRecordData).TypeCovered)\n                        {\n                            case DnsResourceRecordType.NSEC:\n                            case DnsResourceRecordType.NSEC3:\n                                newAuthority.Add(record);\n                                break;\n                        }\n                        break;\n                }\n            }\n\n            DnsDatagram lastResponse = response;\n            bool isAuthoritativeAnswer = response.AuthoritativeAnswer;\n            DnsResourceRecord lastRR = response.GetLastAnswerRecord();\n            EDnsOption[] eDnsClientSubnetOption = null;\n            DnsDatagram newResponse = null;\n            double responseRtt = 0.0;\n\n            if (response.Metadata is not null)\n                responseRtt = response.Metadata.RoundTripTime;\n\n            if (_eDnsClientSubnet)\n            {\n                EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                if (requestECS is not null)\n                    eDnsClientSubnetOption = [new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS)];\n            }\n\n            int queryCount = 0;\n            do\n            {\n                string cnameDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain;\n                if (lastRR.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase))\n                    break; //loop detected\n\n                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);\n\n                //query authoritative zone first\n                newResponse = await AuthoritativeQueryAsync(newRequest, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, remoteEP);\n                if (newResponse is null)\n                {\n                    //not found in auth zone\n                    if (newRequest.RecursionDesired && isRecursionAllowed)\n                    {\n                        //do recursion\n                        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\n                        if (newResponse is null)\n                            return null; //drop request\n\n                        isAuthoritativeAnswer = false;\n                    }\n                    else\n                    {\n                        //break since no recursion allowed/desired\n                        break;\n                    }\n                }\n                else if ((newResponse.Answer.Count > 0) && (newResponse.GetLastAnswerRecord() is DnsResourceRecord lastAnswer) && ((lastAnswer.Type == DnsResourceRecordType.ANAME) || (lastAnswer.Type == DnsResourceRecordType.ALIAS)))\n                {\n                    newResponse = await ProcessANAMEAsync(request, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                    if (newResponse is null)\n                        return null; //drop request\n                }\n                else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0))\n                {\n                    //found delegated/forwarded zone\n                    DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord();\n                    switch (firstAuthority.Type)\n                    {\n                        case DnsResourceRecordType.NS:\n                            if (newRequest.RecursionDesired && isRecursionAllowed)\n                            {\n                                //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolveDnsCache\n                                newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                                if (newResponse is null)\n                                    return null; //drop request\n\n                                isAuthoritativeAnswer = false;\n                            }\n\n                            break;\n\n                        case DnsResourceRecordType.FWD:\n                            //do conditional forwarding\n                            newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                            if (newResponse is null)\n                                return null; //drop request\n\n                            isAuthoritativeAnswer = false;\n                            break;\n\n                        case DnsResourceRecordType.APP:\n                            newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                            if (newResponse is null)\n                                return null; //drop request\n\n                            break;\n                    }\n                }\n\n                if (newResponse.Metadata is not null)\n                    responseRtt += newResponse.Metadata.RoundTripTime;\n\n                //check last response\n                if (newResponse.Answer.Count == 0)\n                    break; //cannot proceed to resolve further\n\n                lastRR = newResponse.GetLastAnswerRecord();\n                if (lastRR.Type != DnsResourceRecordType.CNAME)\n                {\n                    newAnswer.AddRange(newResponse.Answer);\n                    break; //cname was resolved\n                }\n\n                bool foundRepeat = false;\n\n                foreach (DnsResourceRecord newResponseAnswerRecord in newResponse.Answer)\n                {\n                    if ((newResponseAnswerRecord.Type == DnsResourceRecordType.CNAME) || (newResponseAnswerRecord.Type == DnsResourceRecordType.DNAME))\n                    {\n                        foreach (DnsResourceRecord answerRecord in newAnswer)\n                        {\n                            if (newResponseAnswerRecord.Equals(answerRecord))\n                            {\n                                foundRepeat = true;\n                                break;\n                            }\n                        }\n\n                        if (foundRepeat)\n                            break;\n                    }\n\n                    newAnswer.Add(newResponseAnswerRecord);\n                }\n\n                if (foundRepeat)\n                    break; //loop detected\n\n                lastResponse = newResponse;\n            }\n            while (++queryCount < MAX_CNAME_HOPS);\n\n            DnsResponseCode rcode;\n            IReadOnlyList<DnsResourceRecord> authority;\n            IReadOnlyList<DnsResourceRecord> additional;\n\n            if (newResponse is null)\n            {\n                //no recursion available\n                rcode = DnsResponseCode.NoError;\n\n                if (newAuthority.Count == 0)\n                {\n                    authority = lastResponse.Authority;\n                }\n                else\n                {\n                    newAuthority.AddRange(lastResponse.Authority);\n                    authority = newAuthority;\n                }\n\n                additional = lastResponse.Additional;\n            }\n            else\n            {\n                rcode = newResponse.RCODE;\n\n                if (newAuthority.Count == 0)\n                {\n                    authority = newResponse.Authority;\n                }\n                else\n                {\n                    newAuthority.AddRange(newResponse.Authority);\n                    authority = newAuthority;\n                }\n\n                additional = newResponse.Additional;\n            }\n\n            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 };\n            finalResponse.SetMetadata(null, responseRtt);\n\n            return finalResponse;\n        }\n\n        private async Task<DnsDatagram> ProcessANAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout)\n        {\n            EDnsOption[] eDnsClientSubnetOption = null;\n\n            if (_eDnsClientSubnet)\n            {\n                EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                if (requestECS is not null)\n                    eDnsClientSubnetOption = [new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS)];\n            }\n\n            Queue<Task<IReadOnlyList<DnsResourceRecord>>> resolveQueue = new Queue<Task<IReadOnlyList<DnsResourceRecord>>>();\n\n            async Task<IReadOnlyList<DnsResourceRecord>> ResolveANAMEAsync(DnsResourceRecord anameRR, int queryCount = 0)\n            {\n                string lastDomain = (anameRR.RDATA as DnsANAMERecordData).Domain;\n                if (anameRR.Name.Equals(lastDomain, StringComparison.OrdinalIgnoreCase))\n                    return null; //loop detected\n\n                do\n                {\n                    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);\n\n                    //query authoritative zone first\n                    DnsDatagram newResponse = await AuthoritativeQueryAsync(newRequest, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, remoteEP);\n                    if (newResponse is null)\n                    {\n                        //not found in auth zone; do recursion\n                        newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                        if (newResponse is null)\n                            return null; //drop request\n                    }\n                    else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0))\n                    {\n                        //found delegated/forwarded zone\n                        DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord();\n                        switch (firstAuthority.Type)\n                        {\n                            case DnsResourceRecordType.NS:\n                                //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache\n                                newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                                if (newResponse is null)\n                                    return null; //drop request\n\n                                break;\n\n                            case DnsResourceRecordType.FWD:\n                                //do conditional forwarding\n                                newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                                if (newResponse is null)\n                                    return null; //drop request\n\n                                break;\n\n                            case DnsResourceRecordType.APP:\n                                newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                                if (newResponse is null)\n                                    return null; //drop request\n\n                                break;\n                        }\n                    }\n\n                    //check new response\n                    if (newResponse.RCODE != DnsResponseCode.NoError)\n                        return null; //cannot proceed to resolve further\n\n                    if (newResponse.Answer.Count == 0)\n                        return Array.Empty<DnsResourceRecord>(); //NO DATA\n\n                    DnsResourceRecordType questionType = request.Question[0].Type;\n                    DnsResourceRecord lastRR = newResponse.GetLastAnswerRecord();\n                    if (lastRR.Type == questionType)\n                    {\n                        //found final answer\n                        List<DnsResourceRecord> answers = new List<DnsResourceRecord>();\n\n                        foreach (DnsResourceRecord answer in newResponse.Answer)\n                        {\n                            if (answer.Type != questionType)\n                                continue;\n\n                            if (anameRR.TTL < answer.TTL)\n                                answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, anameRR.TTL, answer.RDATA));\n                            else\n                                answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, answer.TTL, answer.RDATA));\n                        }\n\n                        return answers;\n                    }\n\n                    switch (lastRR.Type)\n                    {\n                        case DnsResourceRecordType.ANAME:\n                        case DnsResourceRecordType.ALIAS:\n                            if (newResponse.Answer.Count == 1)\n                            {\n                                lastDomain = (lastRR.RDATA as DnsANAMERecordData).Domain;\n                            }\n                            else\n                            {\n                                //resolve multiple ANAME records async\n                                queryCount++; //increment since one query was done already\n\n                                foreach (DnsResourceRecord newAnswer in newResponse.Answer)\n                                    resolveQueue.Enqueue(ResolveANAMEAsync(newAnswer, queryCount));\n\n                                return Array.Empty<DnsResourceRecord>();\n                            }\n                            break;\n\n                        case DnsResourceRecordType.CNAME:\n                            lastDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain;\n                            break;\n\n                        default:\n                            //aname/cname was resolved, but no answer found\n                            return Array.Empty<DnsResourceRecord>();\n                    }\n                }\n                while (++queryCount < MAX_CNAME_HOPS);\n\n                //max hops limit crossed\n                return null;\n            }\n\n            List<DnsResourceRecord> responseAnswer = new List<DnsResourceRecord>();\n\n            foreach (DnsResourceRecord answer in response.Answer)\n            {\n                switch (answer.Type)\n                {\n                    case DnsResourceRecordType.ANAME:\n                    case DnsResourceRecordType.ALIAS:\n                        resolveQueue.Enqueue(ResolveANAMEAsync(answer));\n                        break;\n\n                    default:\n                        if (resolveQueue.Count == 0)\n                            responseAnswer.Add(answer);\n\n                        break;\n                }\n            }\n\n            bool foundErrors = false;\n\n            while (resolveQueue.Count > 0)\n            {\n                IReadOnlyList<DnsResourceRecord> records = await resolveQueue.Dequeue();\n                if (records is null)\n                    foundErrors = true;\n                else if (records.Count > 0)\n                    responseAnswer.AddRange(records);\n            }\n\n            DnsResponseCode rcode = DnsResponseCode.NoError;\n            IReadOnlyList<DnsResourceRecord> authority = null;\n\n            if (responseAnswer.Count == 0)\n            {\n                if (foundErrors)\n                {\n                    rcode = DnsResponseCode.ServerFailure;\n                }\n                else\n                {\n                    authority = response.Authority;\n\n                    //update last used on\n                    DateTime utcNow = DateTime.UtcNow;\n\n                    foreach (DnsResourceRecord record in authority)\n                        record.GetAuthGenericRecordInfo().LastUsedOn = utcNow;\n                }\n            }\n\n            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 };\n        }\n\n        private async Task<bool> IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol)\n        {\n            if (request.Question.Count > 0)\n            {\n                DnsQuestionRecord question = request.Question[0];\n                if (question.Type == DnsResourceRecordType.DS)\n                {\n                    //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\n                    DnsQuestionRecord newQuestion = new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN);\n                    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);\n                }\n            }\n\n            if (_enableBlocking)\n            {\n                if (_blockingBypassList is not null)\n                {\n                    IPAddress remoteIP = remoteEP.Address;\n\n                    foreach (NetworkAddress network in _blockingBypassList)\n                    {\n                        if (network.Contains(remoteIP))\n                            return true;\n                    }\n                }\n\n                if (_allowedZoneManager.IsAllowed(request) || _blockListZoneManager.IsAllowed(request))\n                    return true;\n            }\n\n            foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers)\n            {\n                try\n                {\n                    if (await blockingHandler.IsAllowedAsync(request, remoteEP))\n                        return true;\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(remoteEP, protocol, ex);\n                }\n            }\n\n            return false;\n        }\n\n        private async Task<DnsDatagram> ProcessBlockedQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol)\n        {\n            if (_enableBlocking)\n            {\n                DnsDatagram response = _blockedZoneManager.Query(request);\n                if (response is null)\n                {\n                    //domain not blocked in blocked zone\n                    response = _blockListZoneManager.Query(request); //check in block list zone\n                    if (response is not null)\n                    {\n                        //domain is blocked in block list zone\n                        response.Tag = DnsServerResponseType.Blocked;\n                        return response;\n                    }\n\n                    //domain not blocked in block list zone; continue to check app blocking handlers\n                }\n                else\n                {\n                    //domain is blocked in blocked zone\n                    DnsQuestionRecord question = request.Question[0];\n\n                    string GetBlockedDomain()\n                    {\n                        DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord();\n                        if ((firstAuthority is not null) && (firstAuthority.Type == DnsResourceRecordType.SOA))\n                            return firstAuthority.Name;\n                        else\n                            return question.Name;\n                    }\n\n                    if (_allowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT))\n                    {\n                        //return meta data\n                        string blockedDomain = GetBlockedDomain();\n\n                        IReadOnlyList<DnsResourceRecord> answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _blockingAnswerTtl, new DnsTXTRecordData(\"source=blocked-zone; domain=\" + blockedDomain))];\n\n                        return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked };\n                    }\n                    else\n                    {\n                        string blockedDomain = null;\n                        EDnsOption[] options = null;\n\n                        if (_allowTxtBlockingReport && (request.EDNS is not null))\n                        {\n                            blockedDomain = GetBlockedDomain();\n                            options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, \"source=blocked-zone; domain=\" + blockedDomain))];\n                        }\n\n                        IReadOnlyCollection<DnsARecordData> aRecords;\n                        IReadOnlyCollection<DnsAAAARecordData> aaaaRecords;\n\n                        switch (_blockingType)\n                        {\n                            case DnsServerBlockingType.AnyAddress:\n                                aRecords = _aRecords;\n                                aaaaRecords = _aaaaRecords;\n                                break;\n\n                            case DnsServerBlockingType.CustomAddress:\n                                aRecords = _customBlockingARecords;\n                                aaaaRecords = _customBlockingAAAARecords;\n                                break;\n\n                            case DnsServerBlockingType.NxDomain:\n                                if (blockedDomain is null)\n                                    blockedDomain = GetBlockedDomain();\n\n                                string parentDomain = AuthZoneManager.GetParentZone(blockedDomain);\n                                if (parentDomain is null)\n                                    parentDomain = string.Empty;\n\n                                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 };\n\n                            default:\n                                throw new InvalidOperationException();\n                        }\n\n                        IReadOnlyList<DnsResourceRecord> answer;\n                        IReadOnlyList<DnsResourceRecord> authority = null;\n\n                        switch (question.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                                {\n                                    if (aRecords.Count > 0)\n                                    {\n                                        DnsResourceRecord[] rrList = new DnsResourceRecord[aRecords.Count];\n                                        int i = 0;\n\n                                        foreach (DnsARecordData record in aRecords)\n                                            rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _blockingAnswerTtl, record);\n\n                                        answer = rrList;\n                                    }\n                                    else\n                                    {\n                                        answer = null;\n                                        authority = response.Authority;\n                                    }\n                                }\n                                break;\n\n                            case DnsResourceRecordType.AAAA:\n                                {\n                                    if (aaaaRecords.Count > 0)\n                                    {\n                                        DnsResourceRecord[] rrList = new DnsResourceRecord[aaaaRecords.Count];\n                                        int i = 0;\n\n                                        foreach (DnsAAAARecordData record in aaaaRecords)\n                                            rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _blockingAnswerTtl, record);\n\n                                        answer = rrList;\n                                    }\n                                    else\n                                    {\n                                        answer = null;\n                                        authority = response.Authority;\n                                    }\n                                }\n                                break;\n\n                            default:\n                                answer = response.Answer;\n                                authority = response.Authority;\n                                break;\n                        }\n\n                        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 };\n                    }\n                }\n            }\n\n            foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers)\n            {\n                try\n                {\n                    DnsDatagram appBlockedResponse = await blockingHandler.ProcessRequestAsync(request, remoteEP);\n                    if (appBlockedResponse is not null)\n                    {\n                        if (appBlockedResponse.Tag is null)\n                            appBlockedResponse.Tag = DnsServerResponseType.Blocked;\n\n                        return appBlockedResponse;\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(remoteEP, protocol, ex);\n                }\n            }\n\n            return null;\n        }\n\n        private async Task<DnsDatagram> ProcessRecursiveQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, IReadOnlyList<DnsResourceRecord> conditionalForwarders, bool dnssecValidation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout)\n        {\n            bool isAllowed;\n\n            if (cacheRefreshOperation)\n            {\n                //cache refresh operation should be able to refresh all the records in cache\n                //this is since a blocked CNAME record could still be used by an allowed domain name and so must resolve\n                isAllowed = true;\n            }\n            else\n            {\n                isAllowed = await IsAllowedAsync(request, remoteEP, protocol);\n                if (!isAllowed)\n                {\n                    DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(request, remoteEP, protocol);\n                    if (blockedResponse is not null)\n                        return blockedResponse;\n                }\n            }\n\n            DnsDatagram response = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n            if (response is null)\n                return null; //drop request\n\n            if (response.Answer.Count > 0)\n            {\n                DnsResourceRecordType questionType = request.Question[0].Type;\n                DnsResourceRecord lastRR = response.GetLastAnswerRecord();\n\n                if ((lastRR.Type != questionType) && (lastRR.Type == DnsResourceRecordType.CNAME) && (questionType != DnsResourceRecordType.ANY))\n                {\n                    response = await ProcessCNAMEAsync(request, response, remoteEP, protocol, true, skipDnsAppAuthoritativeRequestHandlers, clientTimeout);\n                    if (response is null)\n                        return null; //drop request\n                }\n\n                if (!isAllowed)\n                {\n                    //check for CNAME cloaking\n                    for (int i = 0; i < response.Answer.Count; i++)\n                    {\n                        DnsResourceRecord record = response.Answer[i];\n\n                        if (record.Type != DnsResourceRecordType.CNAME)\n                            break; //no further CNAME records exists\n\n                        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);\n\n                        if (request.Metadata is not null)\n                            newRequest.SetMetadata(request.Metadata.NameServer);\n\n                        //check allowed zone\n                        isAllowed = await IsAllowedAsync(newRequest, remoteEP, protocol);\n                        if (isAllowed)\n                            break; //CNAME is in allowed zone\n\n                        //check blocked zone and block list zone\n                        DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(newRequest, remoteEP, protocol);\n                        if (blockedResponse is not null)\n                        {\n                            //found cname cloaking\n                            List<DnsResourceRecord> answer = new List<DnsResourceRecord>();\n\n                            //copy current and previous CNAME records\n                            for (int j = 0; j <= i; j++)\n                                answer.Add(response.Answer[j]);\n\n                            //copy last response answers\n                            answer.AddRange(blockedResponse.Answer);\n\n                            //include blocked response additional section to pass on Extended DNS Errors\n                            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 };\n                        }\n                    }\n                }\n            }\n\n            if (response.Tag is null)\n            {\n                if (response.IsBlockedResponse())\n                    response.Tag = DnsServerResponseType.UpstreamBlocked;\n            }\n            else if ((DnsServerResponseType)response.Tag == DnsServerResponseType.Cached)\n            {\n                if (response.IsBlockedResponse())\n                    response.Tag = DnsServerResponseType.UpstreamBlockedCached;\n            }\n\n            return response;\n        }\n\n        private async Task<DnsDatagram> RecursiveResolveAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList<DnsResourceRecord> conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout)\n        {\n            DnsQuestionRecord question = request.Question[0];\n            NetworkAddress eDnsClientSubnet = null;\n            bool advancedForwardingClientSubnet = false; //this feature is used by Advanced Forwarding app to cache response per network group\n\n            if (_eDnsClientSubnet)\n            {\n                EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                if (requestECS is null)\n                {\n                    if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork))\n                    {\n                        //set ipv4 override shadow ECS option\n                        eDnsClientSubnet = _eDnsClientSubnetIpv4Override;\n                        request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                    }\n                    else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6))\n                    {\n                        //set ipv6 override shadow ECS option\n                        eDnsClientSubnet = _eDnsClientSubnetIpv6Override;\n                        request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                    }\n                    else if (!NetUtilities.IsPrivateIP(remoteEP.Address))\n                    {\n                        //set shadow ECS option\n                        switch (remoteEP.AddressFamily)\n                        {\n                            case AddressFamily.InterNetwork:\n                                eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv4PrefixLength);\n                                request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                                break;\n\n                            case AddressFamily.InterNetworkV6:\n                                eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv6PrefixLength);\n                                request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                                break;\n\n                            default:\n                                request.ShadowHideEDnsClientSubnetOption();\n                                break;\n                        }\n                    }\n                }\n                else if ((requestECS.Family != EDnsClientSubnetAddressFamily.IPv4) && (requestECS.Family != EDnsClientSubnetAddressFamily.IPv6))\n                {\n                    return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative };\n                }\n                else if (requestECS.AdvancedForwardingClientSubnet)\n                {\n                    //request from Advanced Forwarding app\n                    advancedForwardingClientSubnet = true;\n                    eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength);\n                }\n                else if ((requestECS.SourcePrefixLength == 0) || NetUtilities.IsPrivateIP(requestECS.Address))\n                {\n                    //disable ECS option\n                    request.ShadowHideEDnsClientSubnetOption();\n                }\n                else if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork))\n                {\n                    //set ipv4 override shadow ECS option\n                    eDnsClientSubnet = _eDnsClientSubnetIpv4Override;\n                    request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                }\n                else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6))\n                {\n                    //set ipv6 override shadow ECS option\n                    eDnsClientSubnet = _eDnsClientSubnetIpv6Override;\n                    request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                }\n                else\n                {\n                    //use ECS from client request\n                    switch (requestECS.Family)\n                    {\n                        case EDnsClientSubnetAddressFamily.IPv4:\n                            eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv4PrefixLength));\n                            request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                            break;\n\n                        case EDnsClientSubnetAddressFamily.IPv6:\n                            eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv6PrefixLength));\n                            request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet);\n                            break;\n                    }\n                }\n            }\n            else\n            {\n                //ECS feature disabled\n                EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                if (requestECS is not null)\n                {\n                    advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet;\n                    if (advancedForwardingClientSubnet)\n                        eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); //request from Advanced Forwarding app\n                    else\n                        request.ShadowHideEDnsClientSubnetOption(); //hide ECS option\n                }\n            }\n\n            if (!cachePrefetchOperation && !cacheRefreshOperation)\n            {\n                //query cache zone to see if answer available\n                DnsDatagram cacheResponse = await QueryCacheAsync(request, false, false);\n                if (cacheResponse is not null)\n                {\n                    if (_cachePrefetchTrigger > 0)\n                    {\n                        //inspect response TTL values to decide if prefetch trigger is needed\n                        foreach (DnsResourceRecord answer in cacheResponse.Answer)\n                        {\n                            if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && ((answer.TTL <= _cachePrefetchTrigger) || answer.IsStale))\n                            {\n                                //trigger prefetch async for this specific answer record\n                                _ = PrefetchCacheAsync(new DnsQuestionRecord(answer.Name, question.Type, question.Class), remoteEP, conditionalForwarders);\n                                break;\n                            }\n                        }\n                    }\n\n                    return cacheResponse;\n                }\n            }\n\n            //recursion with locking\n            TaskCompletionSource<RecursiveResolveResponse> resolverTaskCompletionSource = new TaskCompletionSource<RecursiveResolveResponse>();\n            Task<RecursiveResolveResponse> resolverTask = _resolverTasks.GetOrAdd(GetResolverQueryKey(question, eDnsClientSubnet), resolverTaskCompletionSource.Task);\n\n            if (resolverTask.Equals(resolverTaskCompletionSource.Task))\n            {\n                //got new resolver task added so question is not being resolved; do recursive resolution in another task on resolver thread pool\n                if (!_resolverTaskPool.TryQueueTask(delegate (object state)\n                    {\n                        return RecursiveResolverBackgroundTaskAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource);\n                    })\n                )\n                {\n                    //resolver queue full\n                    if (!_resolverTasks.TryRemove(GetResolverQueryKey(question, eDnsClientSubnet), out _)) //remove recursion lock entry\n                        throw new InvalidOperationException();\n\n                    return null; //drop request\n                }\n            }\n\n            //request is being recursively resolved by another thread\n\n            if (cachePrefetchOperation)\n                return null; //return null as prefetch worker thread does not need valid response and thus does not need to wait\n\n            if (_serveStale)\n            {\n                int waitTimeout = Math.Min(_serveStaleMaxWaitTime, clientTimeout - SERVE_STALE_TIME_DIFFERENCE); //200ms before client timeout or max 1800ms [RFC 8767]\n                using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource();\n\n                //wait till short timeout for response\n                if ((waitTimeout > 0) && ((await Task.WhenAny(resolverTask, Task.Delay(waitTimeout, timeoutCancellationTokenSource.Token)) == resolverTask) || (resolverTask.Status == TaskStatus.RanToCompletion)))\n                {\n                    //resolver signaled\n                    timeoutCancellationTokenSource.Cancel(); //to stop delay task\n\n                    RecursiveResolveResponse response = await resolverTask;\n\n                    if (response is not null)\n                        return PrepareRecursiveResolveResponse(request, response);\n\n                    //resolver had exception\n                }\n                else\n                {\n                    //wait timed out\n\n                    //query cache zone to return stale answer (if available) as per RFC 8767\n                    DnsDatagram staleResponse = await QueryCacheAsync(request, true, false);\n                    if (staleResponse is not null)\n                        return staleResponse;\n\n                    //no stale record was found\n                    //wait till full timeout before responding as ServerFailure\n                    int timeout = clientTimeout - waitTimeout;\n\n                    if ((await Task.WhenAny(resolverTask, Task.Delay(timeout, timeoutCancellationTokenSource.Token)) == resolverTask) || (resolverTask.Status == TaskStatus.RanToCompletion))\n                    {\n                        //resolver signaled\n                        timeoutCancellationTokenSource.Cancel(); //to stop delay task\n\n                        RecursiveResolveResponse response = await resolverTask;\n\n                        if (response is not null)\n                            return PrepareRecursiveResolveResponse(request, response);\n\n                        //resolver had exception\n                    }\n                }\n            }\n            else\n            {\n                using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource();\n\n                //wait till full client timeout for response\n                if ((await Task.WhenAny(resolverTask, Task.Delay(clientTimeout, timeoutCancellationTokenSource.Token)) == resolverTask) || (resolverTask.Status == TaskStatus.RanToCompletion))\n                {\n                    //resolver signaled\n                    timeoutCancellationTokenSource.Cancel(); //to stop delay task\n\n                    RecursiveResolveResponse response = await resolverTask;\n\n                    if (response is not null)\n                        return PrepareRecursiveResolveResponse(request, response);\n\n                    //resolver had exception\n                }\n            }\n\n            //no response available; respond with ServerFailure\n            EDnsOption[] options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, \"Waiting for resolver. Please try again.\"))];\n            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);\n        }\n\n        private async Task RecursiveResolverBackgroundTaskAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IReadOnlyList<DnsResourceRecord> conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, TaskCompletionSource<RecursiveResolveResponse> taskCompletionSource)\n        {\n            try\n            {\n                //recursive resolve and update cache\n                IDnsCache dnsCache;\n\n                if (cachePrefetchOperation || cacheRefreshOperation)\n                    dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question);\n                else if (skipDnsAppAuthoritativeRequestHandlers || advancedForwardingClientSubnet)\n                    dnsCache = _dnsCacheSkipDnsApps; //to prevent request reaching apps again\n                else\n                    dnsCache = _dnsCache;\n\n                DnsDatagram response;\n\n                if (conditionalForwarders is not null)\n                {\n                    if (conditionalForwarders.Count > 0)\n                    {\n                        //do priority based conditional forwarding\n                        response = await PriorityConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, skipDnsAppAuthoritativeRequestHandlers, conditionalForwarders);\n                    }\n                    else\n                    {\n                        //do force recursive resolution\n                        response = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                        {\n                            return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, true, cancellationToken: cancellationToken1);\n                        }, RECURSIVE_RESOLUTION_TIMEOUT);\n                    }\n                }\n                else\n                {\n                    //do default recursive resolution\n                    response = await DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, dnssecValidation, skipDnsAppAuthoritativeRequestHandlers);\n                }\n\n                switch (response.RCODE)\n                {\n                    case DnsResponseCode.NoError:\n                    case DnsResponseCode.NxDomain:\n                    case DnsResponseCode.YXDomain:\n                        taskCompletionSource.SetResult(new RecursiveResolveResponse(response, response));\n                        break;\n\n                    default:\n                        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));\n                }\n            }\n            catch (Exception ex)\n            {\n                if (_resolverLog is not null)\n                {\n                    string strForwarders = null;\n\n                    if (conditionalForwarders is not null)\n                    {\n                        //empty conditional forwarder array is used to force recursive resolution\n                        if (conditionalForwarders.Count > 0)\n                        {\n                            foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)\n                            {\n                                NameServerAddress nameServer = (conditionalForwarder.RDATA as DnsForwarderRecordData).NameServer;\n\n                                if (strForwarders is null)\n                                    strForwarders = nameServer.ToString();\n                                else\n                                    strForwarders += \", \" + nameServer.ToString();\n                            }\n                        }\n                    }\n                    else if ((_forwarders is not null) && (_forwarders.Count > 0))\n                    {\n                        foreach (NameServerAddress nameServer in _forwarders)\n                        {\n                            if (strForwarders is null)\n                                strForwarders = nameServer.ToString();\n                            else\n                                strForwarders += \", \" + nameServer.ToString();\n                        }\n                    }\n\n                    _resolverLog.Write(\"DNS Server failed to resolve the request '\" + question.ToString() + \"'\" + (strForwarders is null ? \"\" : \" using forwarders: \" + strForwarders) + \".\\r\\n\" + ex.ToString());\n                }\n\n                //fetch failure/stale response to signal; reset stale records\n                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));\n                DnsDatagram cacheResponse = await QueryCacheAsync(cacheRequest, _serveStale, _serveStale);\n                if (cacheResponse is not null)\n                {\n                    //signal failure/stale response\n                    if (!dnssecValidation || cacheResponse.AuthenticData)\n                    {\n                        //no dnssec validation enabled OR cache response is validated data\n                        taskCompletionSource.SetResult(new RecursiveResolveResponse(cacheResponse, cacheResponse));\n                    }\n                    else\n                    {\n                        //dnssec validation enabled; cache response may be a bogus/failure response\n\n                        static bool HasBogusRecords(IReadOnlyList<DnsResourceRecord> records)\n                        {\n                            foreach (DnsResourceRecord record in records)\n                            {\n                                switch (record.DnssecStatus)\n                                {\n                                    case DnssecStatus.Disabled:\n                                    case DnssecStatus.Secure:\n                                    case DnssecStatus.Insecure:\n                                    case DnssecStatus.Indeterminate:\n                                        break;\n\n                                    default:\n                                        return true;\n                                }\n                            }\n\n                            return false;\n                        }\n\n                        bool isFailureResponse = false;\n\n                        switch (cacheResponse.RCODE)\n                        {\n                            case DnsResponseCode.NoError:\n                            case DnsResponseCode.NxDomain:\n                            case DnsResponseCode.YXDomain:\n                                isFailureResponse = HasBogusRecords(cacheResponse.Answer);\n                                if (!isFailureResponse)\n                                    isFailureResponse = HasBogusRecords(cacheResponse.Authority);\n\n                                break;\n\n                            default:\n                                isFailureResponse = true;\n                                break;\n                        }\n\n                        if (isFailureResponse)\n                        {\n                            //return failure response\n                            List<EDnsOption> options;\n\n                            if ((cacheResponse.EDNS is not null) && (cacheResponse.EDNS.Options.Count > 0))\n                            {\n                                options = new List<EDnsOption>(cacheResponse.EDNS.Options.Count);\n\n                                foreach (EDnsOption option in cacheResponse.EDNS.Options)\n                                {\n                                    if (option.Code == EDnsOptionCode.EXTENDED_DNS_ERROR)\n                                        options.Add(option);\n                                }\n                            }\n                            else\n                            {\n                                options = null;\n                            }\n\n                            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);\n\n                            taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, cacheResponse));\n                        }\n                        else\n                        {\n                            //return cached stale answer\n                            taskCompletionSource.SetResult(new RecursiveResolveResponse(cacheResponse, cacheResponse));\n                        }\n                    }\n                }\n                else\n                {\n                    IReadOnlyList<EDnsOption> options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, \"Resolver exception\"))];\n                    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);\n\n                    taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse));\n                }\n            }\n            finally\n            {\n                _resolverTasks.TryRemove(GetResolverQueryKey(question, eDnsClientSubnet), out _);\n            }\n        }\n\n        private async Task<DnsDatagram> DefaultRecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, IDnsCache dnsCache, bool dnssecValidation, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default)\n        {\n            IReadOnlyList<NameServerAddress> forwarders = _forwarders;\n\n            if ((forwarders is not null) && (forwarders.Count > 0))\n            {\n                //use forwarders\n                if (_concurrentForwarding)\n                {\n                    if (_proxy is null)\n                    {\n                        //recursive resolve forwarders only when proxy is null else let proxy resolve it to allow using .onion or private domains\n                        List<NameServerAddress> newForwarders = new List<NameServerAddress>(forwarders.Count);\n                        List<Task<NameServerAddress>> resolveTasks = new List<Task<NameServerAddress>>(forwarders.Count);\n\n                        foreach (NameServerAddress forwarder in forwarders)\n                        {\n                            if (forwarder.IsIPEndPointStale)\n                            {\n                                //refresh forwarder IPEndPoint if stale\n                                resolveTasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1)\n                                {\n                                    await forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1);\n                                    return forwarder;\n                                }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken));\n                            }\n                            else\n                            {\n                                newForwarders.Add(forwarder);\n                            }\n                        }\n\n                        Exception lastException = null;\n\n                        foreach (Task<NameServerAddress> resolveTask in resolveTasks)\n                        {\n                            try\n                            {\n                                newForwarders.Add(await resolveTask);\n                            }\n                            catch (Exception ex)\n                            {\n                                lastException = ex;\n                                _resolverLog?.Write(ex);\n                            }\n                        }\n\n                        if (newForwarders.Count < 1)\n                            throw new DnsServerException(\"Failed to resolve forwarder domain name for all forwarders: \" + forwarders.Join(), lastException);\n\n                        forwarders = newForwarders;\n                    }\n\n                    //query forwarders and update cache\n                    DnsClient dnsClient = new DnsClient(forwarders);\n\n                    dnsClient.Cache = dnsCache;\n                    dnsClient.Proxy = _proxy;\n                    dnsClient.PreferIPv6 = _preferIPv6;\n                    dnsClient.RandomizeName = _randomizeName;\n                    dnsClient.Retries = _forwarderRetries;\n                    dnsClient.Timeout = _forwarderTimeout;\n                    dnsClient.Concurrency = _forwarderConcurrency;\n                    dnsClient.UdpPayloadSize = _udpPayloadSize;\n                    dnsClient.DnssecValidation = dnssecValidation;\n                    dnsClient.EDnsClientSubnet = eDnsClientSubnet;\n                    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\n\n                    return await dnsClient.ResolveAsync(question, cancellationToken);\n                }\n                else\n                {\n                    //do sequentially ordered forwarding\n                    Exception lastException = null;\n\n                    foreach (NameServerAddress forwarder in forwarders)\n                    {\n                        if (_proxy is null)\n                        {\n                            //recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains\n                            if (forwarder.IsIPEndPointStale)\n                            {\n                                try\n                                {\n                                    //refresh forwarder IPEndPoint if stale\n                                    await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                                    {\n                                        return forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1);\n                                    }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken);\n                                }\n                                catch (Exception ex)\n                                {\n                                    //failed to refresh forwarder IP address; try next forwarder\n                                    lastException = ex;\n                                    _resolverLog?.Write(ex);\n                                    continue;\n                                }\n                            }\n                        }\n\n                        //query forwarder and update cache\n                        DnsClient dnsClient = new DnsClient(forwarder);\n\n                        dnsClient.Cache = dnsCache;\n                        dnsClient.Proxy = _proxy;\n                        dnsClient.PreferIPv6 = _preferIPv6;\n                        dnsClient.RandomizeName = _randomizeName;\n                        dnsClient.Retries = _forwarderRetries;\n                        dnsClient.Timeout = _forwarderTimeout;\n                        dnsClient.Concurrency = _forwarderConcurrency;\n                        dnsClient.UdpPayloadSize = _udpPayloadSize;\n                        dnsClient.DnssecValidation = dnssecValidation;\n                        dnsClient.EDnsClientSubnet = eDnsClientSubnet;\n                        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\n\n                        try\n                        {\n                            return await dnsClient.ResolveAsync(question, cancellationToken);\n                        }\n                        catch (Exception ex)\n                        {\n                            lastException = ex;\n                        }\n\n                        if (dnsCache is not ResolverPrefetchDnsCache)\n                            dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache\n                    }\n\n                    ExceptionDispatchInfo.Capture(lastException).Throw();\n                    throw lastException;\n                }\n            }\n            else\n            {\n                //do recursive resolution\n                return await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                {\n                    return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, true, null, cancellationToken1);\n                }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken);\n            }\n        }\n\n        internal async Task<DnsDatagram> PriorityConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, bool skipDnsAppAuthoritativeRequestHandlers, IReadOnlyList<DnsResourceRecord> conditionalForwarders)\n        {\n            if (conditionalForwarders.Count == 1)\n            {\n                DnsResourceRecord conditionalForwarder = conditionalForwarders[0];\n                return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers);\n            }\n\n            //check for forwarder name server resolution\n            List<Task> resolveTasks = new List<Task>(conditionalForwarders.Count);\n\n            foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)\n            {\n                if (conditionalForwarder.Type != DnsResourceRecordType.FWD)\n                    continue;\n\n                DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData;\n\n                if (forwarder.Forwarder.Equals(\"this-server\", StringComparison.OrdinalIgnoreCase))\n                    continue; //skip resolving\n\n                NetProxy proxy = forwarder.GetProxy(_proxy);\n                if (proxy is null)\n                {\n                    //recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains\n                    if (forwarder.NameServer.IsIPEndPointStale)\n                    {\n                        //refresh forwarder IPEndPoint if stale\n                        resolveTasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                        {\n                            return forwarder.NameServer.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1);\n                        }, RECURSIVE_RESOLUTION_TIMEOUT));\n                    }\n                }\n            }\n\n            Exception lastResolverException = null;\n\n            foreach (Task resolverTask in resolveTasks)\n            {\n                try\n                {\n                    await resolverTask;\n                }\n                catch (Exception ex)\n                {\n                    lastResolverException = ex;\n                    _resolverLog?.Write(ex);\n                }\n            }\n\n            //group by priority\n            Dictionary<byte, List<DnsResourceRecord>> conditionalForwarderGroups = new Dictionary<byte, List<DnsResourceRecord>>(conditionalForwarders.Count);\n            {\n                foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)\n                {\n                    if (conditionalForwarder.Type != DnsResourceRecordType.FWD)\n                        continue;\n\n                    DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData;\n\n                    if (forwarder.NameServer.IsIPEndPointStale)\n                        continue; //skip stale forwarders since they failed to resolve\n\n                    if (conditionalForwarderGroups.TryGetValue(forwarder.Priority, out List<DnsResourceRecord> conditionalForwardersEntry))\n                    {\n                        conditionalForwardersEntry.Add(conditionalForwarder);\n                    }\n                    else\n                    {\n                        conditionalForwardersEntry = new List<DnsResourceRecord>(2)\n                        {\n                            conditionalForwarder\n                        };\n\n                        conditionalForwarderGroups[forwarder.Priority] = conditionalForwardersEntry;\n                    }\n                }\n            }\n\n            if (conditionalForwarderGroups.Count < 1)\n            {\n                List<NameServerAddress> forwarders = new List<NameServerAddress>(conditionalForwarders.Count);\n\n                foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)\n                {\n                    if (conditionalForwarder.Type != DnsResourceRecordType.FWD)\n                        continue;\n\n                    forwarders.Add((conditionalForwarder.RDATA as DnsForwarderRecordData).NameServer);\n                }\n\n                throw new DnsServerException(\"Failed to resolve forwarder domain name for all conditional forwarders: \" + forwarders.Join(), lastResolverException);\n            }\n\n            if (conditionalForwarderGroups.Count == 1)\n            {\n                foreach (KeyValuePair<byte, List<DnsResourceRecord>> conditionalForwardersEntry in conditionalForwarderGroups)\n                    return await ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry.Value, skipDnsAppAuthoritativeRequestHandlers);\n            }\n\n            List<byte> priorities = new List<byte>(conditionalForwarderGroups.Keys);\n            priorities.Sort();\n\n            using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())\n            {\n                CancellationToken currentCancellationToken = cancellationTokenSource.Token;\n\n                DnsDatagram lastResponse = null;\n                Exception lastException = null;\n\n                foreach (byte priority in priorities)\n                {\n                    if (!conditionalForwarderGroups.TryGetValue(priority, out List<DnsResourceRecord> conditionalForwardersEntry))\n                        continue;\n\n                    Task<DnsDatagram> priorityTask = ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken);\n\n                    try\n                    {\n                        DnsDatagram priorityTaskResponse = await priorityTask; //await to get response\n\n                        switch (priorityTaskResponse.RCODE)\n                        {\n                            case DnsResponseCode.NoError:\n                            case DnsResponseCode.NxDomain:\n                            case DnsResponseCode.YXDomain:\n                                cancellationTokenSource.Cancel(); //to stop other priority resolver tasks\n                                return priorityTaskResponse;\n\n                            default:\n                                //keep response\n                                lastResponse = priorityTaskResponse;\n                                break;\n                        }\n                    }\n                    catch (OperationCanceledException)\n                    {\n                        throw;\n                    }\n                    catch (Exception ex)\n                    {\n                        lastException = ex;\n\n                        if (lastException is AggregateException)\n                            lastException = lastException.InnerException;\n                    }\n\n                    if (dnsCache is not ResolverPrefetchDnsCache)\n                        dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache\n                }\n\n                if (lastResponse is not null)\n                    return lastResponse;\n\n                if (lastException is not null)\n                    ExceptionDispatchInfo.Capture(lastException).Throw();\n\n                throw new InvalidOperationException();\n            }\n        }\n\n        private async Task<DnsDatagram> ConcurrentConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, List<DnsResourceRecord> conditionalForwarders, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default)\n        {\n            if (conditionalForwarders.Count == 1)\n            {\n                DnsResourceRecord conditionalForwarder = conditionalForwarders[0];\n                return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, cancellationToken);\n            }\n\n            using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource())\n            {\n                using CancellationTokenRegistration r = cancellationToken.Register(cancellationTokenSource.Cancel);\n\n                CancellationToken currentCancellationToken = cancellationTokenSource.Token;\n                List<Task<DnsDatagram>> tasks = new List<Task<DnsDatagram>>(conditionalForwarders.Count);\n\n                //start worker tasks\n                foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders)\n                {\n                    if (conditionalForwarder.Type != DnsResourceRecordType.FWD)\n                        continue;\n\n                    DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData;\n\n                    tasks.Add(Task.Factory.StartNew(delegate ()\n                    {\n                        return ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, forwarder, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current).Unwrap());\n                }\n\n                //wait for first positive response, or for all tasks to fault\n                DnsDatagram lastResponse = null;\n                Exception lastException = null;\n\n                while (tasks.Count > 0)\n                {\n                    Task<DnsDatagram> completedTask = await Task.WhenAny(tasks);\n\n                    try\n                    {\n                        DnsDatagram taskResponse = await completedTask; //await to get response\n\n                        switch (taskResponse.RCODE)\n                        {\n                            case DnsResponseCode.NoError:\n                            case DnsResponseCode.NxDomain:\n                            case DnsResponseCode.YXDomain:\n                                cancellationTokenSource.Cancel(); //to stop other resolver tasks\n                                return taskResponse;\n\n                            default:\n                                //keep response\n                                lastResponse = taskResponse;\n                                break;\n                        }\n                    }\n                    catch (OperationCanceledException)\n                    {\n                        throw;\n                    }\n                    catch (Exception ex)\n                    {\n                        lastException = ex;\n\n                        if (lastException is AggregateException)\n                            lastException = lastException.InnerException;\n                    }\n\n                    tasks.Remove(completedTask);\n                }\n\n                if (lastResponse is not null)\n                    return lastResponse;\n\n                if (lastException is not null)\n                    ExceptionDispatchInfo.Capture(lastException).Throw();\n\n                throw new InvalidOperationException();\n            }\n        }\n\n        private Task<DnsDatagram> ConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, DnsForwarderRecordData forwarder, string conditionalForwardingZoneCut, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default)\n        {\n            if (forwarder.Forwarder.Equals(\"this-server\", StringComparison.OrdinalIgnoreCase))\n            {\n                //resolve via default recursive resolver with DNSSEC validation preference\n                return DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, forwarder.DnssecValidation, skipDnsAppAuthoritativeRequestHandlers, cancellationToken);\n            }\n            else\n            {\n                //resolve via conditional forwarder\n                DnsClient dnsClient = new DnsClient(forwarder.NameServer);\n\n                dnsClient.Cache = dnsCache;\n                dnsClient.Proxy = forwarder.GetProxy(_proxy);\n                dnsClient.PreferIPv6 = _preferIPv6;\n                dnsClient.RandomizeName = _randomizeName;\n                dnsClient.Retries = _forwarderRetries;\n                dnsClient.Timeout = _forwarderTimeout;\n                dnsClient.Concurrency = _forwarderConcurrency;\n                dnsClient.UdpPayloadSize = _udpPayloadSize;\n                dnsClient.DnssecValidation = forwarder.DnssecValidation;\n                dnsClient.EDnsClientSubnet = eDnsClientSubnet;\n                dnsClient.AdvancedForwardingClientSubnet = advancedForwardingClientSubnet;\n                dnsClient.ConditionalForwardingZoneCut = conditionalForwardingZoneCut;\n\n                return dnsClient.ResolveAsync(question, cancellationToken);\n            }\n        }\n\n        private DnsDatagram PrepareRecursiveResolveResponse(DnsDatagram request, RecursiveResolveResponse resolveResponse)\n        {\n            //get a tailored response for the request\n            bool dnssecOk = request.DnssecOk;\n\n            if (request.CheckingDisabled)\n            {\n                DnsDatagram cdResponse = resolveResponse.CheckingDisabledResponse;\n                bool authenticData = false;\n                IReadOnlyList<DnsResourceRecord> cdAnswer;\n                IReadOnlyList<DnsResourceRecord> cdAuthority;\n                IReadOnlyList<DnsResourceRecord> cdAdditional = RemoveOPTFromAdditional(cdResponse.Additional, dnssecOk);\n                EDnsHeaderFlags ednsFlags;\n\n                if (dnssecOk)\n                {\n                    if (cdResponse.Answer.Count > 0)\n                    {\n                        authenticData = true;\n\n                        foreach (DnsResourceRecord record in cdResponse.Answer)\n                        {\n                            if (record.DnssecStatus != DnssecStatus.Secure)\n                            {\n                                authenticData = false;\n                                break;\n                            }\n                        }\n                    }\n                    else if (cdResponse.Authority.Count > 0)\n                    {\n                        authenticData = true;\n\n                        foreach (DnsResourceRecord record in cdResponse.Authority)\n                        {\n                            if (record.DnssecStatus != DnssecStatus.Secure)\n                            {\n                                authenticData = false;\n                                break;\n                            }\n                        }\n                    }\n\n                    cdAnswer = cdResponse.Answer;\n                    cdAuthority = cdResponse.Authority;\n                    ednsFlags = EDnsHeaderFlags.DNSSEC_OK;\n                }\n                else\n                {\n                    cdAnswer = FilterDnssecRecords(cdResponse.Answer);\n                    cdAuthority = FilterDnssecRecords(cdResponse.Authority);\n                    ednsFlags = EDnsHeaderFlags.None;\n                }\n\n                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);\n                DnsDatagramMetadata metadata = cdResponse.Metadata;\n                if (metadata is not null)\n                    finalCdResponse.SetMetadata(metadata.NameServer, metadata.RoundTripTime);\n\n                return finalCdResponse;\n            }\n\n            DnsResponseCode rCode;\n            DnsDatagram response = resolveResponse.Response;\n            IReadOnlyList<DnsResourceRecord> answer = response.Answer;\n            IReadOnlyList<DnsResourceRecord> authority = response.Authority;\n            IReadOnlyList<DnsResourceRecord> additional = response.Additional;\n\n            switch (response.RCODE)\n            {\n                case DnsResponseCode.NoError:\n                case DnsResponseCode.NxDomain:\n                case DnsResponseCode.YXDomain:\n                    rCode = response.RCODE;\n                    break;\n\n                default:\n                    rCode = DnsResponseCode.ServerFailure;\n                    break;\n            }\n\n            //answer section checks\n            if (!dnssecOk && (answer.Count > 0) && (response.Question[0].Type != DnsResourceRecordType.ANY))\n            {\n                //remove RRSIGs from answer\n                bool foundRRSIG = false;\n\n                foreach (DnsResourceRecord record in answer)\n                {\n                    if (record.Type == DnsResourceRecordType.RRSIG)\n                    {\n                        foundRRSIG = true;\n                        break;\n                    }\n                }\n\n                if (foundRRSIG)\n                {\n                    List<DnsResourceRecord> newAnswer = new List<DnsResourceRecord>(answer.Count);\n\n                    foreach (DnsResourceRecord record in answer)\n                    {\n                        if (record.Type == DnsResourceRecordType.RRSIG)\n                            continue;\n\n                        newAnswer.Add(record);\n                    }\n\n                    answer = newAnswer;\n                }\n            }\n\n            //authority section checks\n            if (!dnssecOk && (authority.Count > 0))\n            {\n                //remove DNSSEC records\n                bool foundDnssecRecords = false;\n                bool foundOther = false;\n\n                foreach (DnsResourceRecord record in authority)\n                {\n                    switch (record.Type)\n                    {\n                        case DnsResourceRecordType.DS:\n                        case DnsResourceRecordType.DNSKEY:\n                        case DnsResourceRecordType.RRSIG:\n                        case DnsResourceRecordType.NSEC:\n                        case DnsResourceRecordType.NSEC3:\n                            foundDnssecRecords = true;\n                            break;\n\n                        default:\n                            foundOther = true;\n                            break;\n                    }\n                }\n\n                if (foundDnssecRecords)\n                {\n                    if (foundOther)\n                    {\n                        List<DnsResourceRecord> newAuthority = new List<DnsResourceRecord>(2);\n\n                        foreach (DnsResourceRecord record in authority)\n                        {\n                            switch (record.Type)\n                            {\n                                case DnsResourceRecordType.DS:\n                                case DnsResourceRecordType.DNSKEY:\n                                case DnsResourceRecordType.RRSIG:\n                                case DnsResourceRecordType.NSEC:\n                                case DnsResourceRecordType.NSEC3:\n                                    break;\n\n                                default:\n                                    newAuthority.Add(record);\n                                    break;\n                            }\n                        }\n\n                        authority = newAuthority;\n                    }\n                    else\n                    {\n                        authority = Array.Empty<DnsResourceRecord>();\n                    }\n                }\n            }\n\n            //additional section checks\n            if (additional.Count > 0)\n            {\n                if ((request.EDNS is not null) && (response.EDNS is not null) && ((response.EDNS.Options.Count > 0) || (response.DnsClientExtendedErrors.Count > 0)))\n                {\n                    //copy options as new OPT and keep other records\n                    List<DnsResourceRecord> newAdditional = new List<DnsResourceRecord>(additional.Count);\n\n                    foreach (DnsResourceRecord record in additional)\n                    {\n                        switch (record.Type)\n                        {\n                            case DnsResourceRecordType.OPT:\n                                continue;\n\n                            case DnsResourceRecordType.RRSIG:\n                            case DnsResourceRecordType.DNSKEY:\n                                if (dnssecOk)\n                                    break;\n\n                                continue;\n                        }\n\n                        newAdditional.Add(record);\n                    }\n\n                    IReadOnlyList<EDnsOption> options;\n\n                    if (response.GetEDnsClientSubnetOption(true) is not null)\n                    {\n                        //response contains ECS\n                        if (request.GetEDnsClientSubnetOption(true) is not null)\n                        {\n                            //request has ECS and type is supported; keep ECS in response\n                            options = response.EDNS.Options;\n                        }\n                        else\n                        {\n                            //cache does not support the qtype so remove ECS from response\n                            if (response.EDNS.Options.Count == 1)\n                            {\n                                options = Array.Empty<EDnsOption>();\n                            }\n                            else\n                            {\n                                List<EDnsOption> newOptions = new List<EDnsOption>(response.EDNS.Options.Count);\n\n                                foreach (EDnsOption option in response.EDNS.Options)\n                                {\n                                    if (option.Code != EDnsOptionCode.EDNS_CLIENT_SUBNET)\n                                        newOptions.Add(option);\n                                }\n\n                                options = newOptions;\n                            }\n                        }\n                    }\n                    else\n                    {\n                        options = response.EDNS.Options;\n                    }\n\n                    if (response.DnsClientExtendedErrors.Count > 0)\n                    {\n                        //add dns client extended errors\n                        List<EDnsOption> newOptions = new List<EDnsOption>(options.Count + response.DnsClientExtendedErrors.Count);\n\n                        newOptions.AddRange(options);\n\n                        foreach (EDnsExtendedDnsErrorOptionData ee in response.DnsClientExtendedErrors)\n                            newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, ee));\n\n                        options = newOptions;\n                    }\n\n                    newAdditional.Add(DnsDatagramEdns.GetOPTFor(_udpPayloadSize, rCode, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options));\n\n                    additional = newAdditional;\n                }\n                else if (response.EDNS is not null)\n                {\n                    //remove OPT from additional\n                    additional = RemoveOPTFromAdditional(additional, dnssecOk);\n                }\n            }\n\n            {\n                bool authenticData = false;\n\n                if (dnssecOk)\n                {\n                    if (answer.Count > 0)\n                    {\n                        authenticData = true;\n\n                        foreach (DnsResourceRecord record in answer)\n                        {\n                            if (record.DnssecStatus != DnssecStatus.Secure)\n                            {\n                                authenticData = false;\n                                break;\n                            }\n                        }\n                    }\n                    else if (authority.Count > 0)\n                    {\n                        authenticData = true;\n\n                        foreach (DnsResourceRecord record in authority)\n                        {\n                            if (record.DnssecStatus != DnssecStatus.Secure)\n                            {\n                                authenticData = false;\n                                break;\n                            }\n                        }\n                    }\n                }\n\n                DnsDatagram finalResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, request.CheckingDisabled, rCode, request.Question, answer, authority, additional);\n                DnsDatagramMetadata metadata = response.Metadata;\n                if (metadata is not null)\n                    finalResponse.SetMetadata(metadata.NameServer, metadata.RoundTripTime);\n\n                return finalResponse;\n            }\n        }\n\n        private static IReadOnlyList<DnsResourceRecord> FilterDnssecRecords(IReadOnlyList<DnsResourceRecord> records)\n        {\n            foreach (DnsResourceRecord record1 in records)\n            {\n                switch (record1.Type)\n                {\n                    case DnsResourceRecordType.RRSIG:\n                    case DnsResourceRecordType.NSEC:\n                    case DnsResourceRecordType.NSEC3:\n                        List<DnsResourceRecord> noDnssecRecords = new List<DnsResourceRecord>();\n\n                        foreach (DnsResourceRecord record2 in records)\n                        {\n                            switch (record2.Type)\n                            {\n                                case DnsResourceRecordType.RRSIG:\n                                case DnsResourceRecordType.NSEC:\n                                case DnsResourceRecordType.NSEC3:\n                                    break;\n\n                                default:\n                                    noDnssecRecords.Add(record2);\n                                    break;\n                            }\n                        }\n\n                        return noDnssecRecords;\n                }\n            }\n\n            return records;\n        }\n\n        private static IReadOnlyList<DnsResourceRecord> RemoveOPTFromAdditional(IReadOnlyList<DnsResourceRecord> additional, bool dnssecOk)\n        {\n            if (additional.Count == 0)\n                return additional;\n\n            if ((additional.Count == 1) && (additional[0].Type == DnsResourceRecordType.OPT))\n                return Array.Empty<DnsResourceRecord>();\n\n            List<DnsResourceRecord> newAdditional = new List<DnsResourceRecord>(additional.Count - 1);\n\n            foreach (DnsResourceRecord record in additional)\n            {\n                switch (record.Type)\n                {\n                    case DnsResourceRecordType.OPT:\n                        continue;\n\n                    case DnsResourceRecordType.RRSIG:\n                    case DnsResourceRecordType.DNSKEY:\n                        if (dnssecOk)\n                            break;\n\n                        continue;\n                }\n\n                newAdditional.Add(record);\n            }\n\n            return newAdditional;\n        }\n\n        private static string GetResolverQueryKey(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet)\n        {\n            if (eDnsClientSubnet is null)\n                return question.ToString();\n\n            return question.ToString() + \" \" + eDnsClientSubnet.ToString();\n        }\n\n        private async Task<DnsDatagram> QueryCacheAsync(DnsDatagram request, bool serveStale, bool resetExpiry)\n        {\n            DnsDatagram cacheResponse = await _cacheZoneManager.QueryAsync(request, serveStale, false, resetExpiry);\n            if (cacheResponse is not null)\n            {\n                if ((cacheResponse.RCODE != DnsResponseCode.NoError) || (cacheResponse.Answer.Count > 0) || (cacheResponse.Authority.Count == 0) || cacheResponse.IsFirstAuthoritySOA())\n                {\n                    cacheResponse.Tag = DnsServerResponseType.Cached;\n\n                    return cacheResponse;\n                }\n            }\n\n            return null;\n        }\n\n        private async Task PrefetchCacheAsync(DnsQuestionRecord question, IPEndPoint remoteEP, IReadOnlyList<DnsResourceRecord> conditionalForwarders)\n        {\n            try\n            {\n                DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, [question]);\n                _ = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, _dnssecValidation, true, false, false, _clientTimeout);\n            }\n            catch (Exception ex)\n            {\n                _resolverLog?.Write(ex);\n            }\n        }\n\n        private async Task RefreshCacheAsync(DnsQuestionRecord neededQuestion, IList<CacheRefreshSample> cacheRefreshSampleList, CacheRefreshSample sample, int sampleQuestionIndex)\n        {\n            try\n            {\n                //refresh cache\n                DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, [neededQuestion]);\n                _ = await ProcessRecursiveQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Udp, sample.ConditionalForwarders, _dnssecValidation, true, false, _clientTimeout);\n            }\n            catch (Exception ex)\n            {\n                _resolverLog?.Write(ex);\n            }\n            finally\n            {\n                cacheRefreshSampleList[sampleQuestionIndex] = sample; //put back into sample list to allow refreshing it again\n            }\n        }\n\n        private async Task<DnsQuestionRecord> GetCacheRefreshNeededQueryAsync(DnsQuestionRecord question, int trigger)\n        {\n            DnsDatagram cacheResponse = await QueryCacheAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), false, false);\n            if (cacheResponse is null)\n                return question; //cache expired so refresh question\n\n            if (cacheResponse.Answer.Count == 0)\n                return null; //dont refresh empty responses\n\n            //inspect response TTL values to decide if refresh is needed\n            foreach (DnsResourceRecord answer in cacheResponse.Answer)\n            {\n                if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && ((answer.TTL <= trigger) || answer.IsStale))\n                    return new DnsQuestionRecord(answer.Name, question.Type, question.Class); //TTL eligible and less than trigger so refresh for current answer record\n            }\n\n            DnsResourceRecord lastRR = cacheResponse.Answer[cacheResponse.Answer.Count - 1];\n            if (lastRR.Type == DnsResourceRecordType.CNAME)\n                return new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, question.Type, question.Class); //found incomplete response; refresh the last CNAME domain name\n\n            return null; //refresh not needed\n        }\n\n        private async void CachePrefetchSamplingTimerCallback(object state)\n        {\n            try\n            {\n                List<KeyValuePair<DnsQuestionRecord, long>> eligibleQueries = _statsManager.GetLastHourEligibleQueries(_cachePrefetchSampleEligibilityHitsPerHour);\n                List<CacheRefreshSample> cacheRefreshSampleList = new List<CacheRefreshSample>(eligibleQueries.Count);\n                int cacheRefreshTrigger = (_cachePrefetchSampleIntervalMinutes + 1) * 60; //extra 1 min to account for any delays in next sampling\n\n                foreach (KeyValuePair<DnsQuestionRecord, long> eligibleQuery in eligibleQueries)\n                {\n                    DnsQuestionRecord eligibleQuerySample = eligibleQuery.Key;\n\n                    switch (eligibleQuerySample.Type)\n                    {\n                        case DnsResourceRecordType.IXFR:\n                        case DnsResourceRecordType.AXFR:\n                        case DnsResourceRecordType.ANY:\n                            continue; //dont refresh these queries\n                    }\n\n                    DnsQuestionRecord refreshQuery = null;\n                    IReadOnlyList<DnsResourceRecord> conditionalForwarders = null;\n\n                    //query auth zone for refresh query\n                    int queryCount = 0;\n                    bool reQueryAuthZone;\n                    do\n                    {\n                        reQueryAuthZone = false;\n\n                        DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { eligibleQuerySample });\n                        DnsDatagram response = await AuthoritativeQueryAsync(request, DnsTransportProtocol.Tcp, true, false, IPENDPOINT_ANY_0);\n                        if (response is null)\n                        {\n                            //zone not hosted; do refresh\n                            refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger);\n                        }\n                        else\n                        {\n                            //zone is hosted; check further\n                            if (response.Answer.Count > 0)\n                            {\n                                DnsResourceRecord lastRR = response.GetLastAnswerRecord();\n                                if ((lastRR.Type == DnsResourceRecordType.CNAME) && (eligibleQuerySample.Type != DnsResourceRecordType.CNAME))\n                                {\n                                    eligibleQuerySample = new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, eligibleQuerySample.Type, eligibleQuerySample.Class);\n                                    reQueryAuthZone = true;\n                                }\n                            }\n                            else if (response.Authority.Count > 0)\n                            {\n                                DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord();\n                                switch (firstAuthority.Type)\n                                {\n                                    case DnsResourceRecordType.NS: //zone is delegated\n                                        refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger);\n                                        conditionalForwarders = Array.Empty<DnsResourceRecord>(); //do forced recursive resolution using empty conditional forwarders\n                                        break;\n\n                                    case DnsResourceRecordType.FWD: //zone is conditional forwarder\n                                        refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger);\n                                        conditionalForwarders = response.Authority; //do conditional forwarding\n                                        break;\n                                }\n                            }\n                        }\n                    }\n                    while (reQueryAuthZone && (++queryCount < MAX_CNAME_HOPS));\n\n                    if (refreshQuery is not null)\n                    {\n                        bool alreadyExists = false;\n\n                        foreach (CacheRefreshSample cacheRefreshSample in cacheRefreshSampleList)\n                        {\n                            if (cacheRefreshSample.SampleQuestion.Equals(refreshQuery))\n                            {\n                                alreadyExists = true;\n                                break; //already exists in sample list\n                            }\n                        }\n\n                        if (!alreadyExists)\n                            cacheRefreshSampleList.Add(new CacheRefreshSample(refreshQuery, conditionalForwarders));\n                    }\n                }\n\n                _cacheRefreshSampleList = cacheRefreshSampleList;\n            }\n            catch (Exception ex)\n            {\n                _log.Write(ex);\n            }\n            finally\n            {\n                lock (_cachePrefetchSamplingTimerLock)\n                {\n                    _cachePrefetchSamplingTimer?.Change(_cachePrefetchSampleIntervalMinutes * 60 * 1000, Timeout.Infinite);\n                }\n            }\n        }\n\n        private async void CachePrefetchRefreshTimerCallback(object state)\n        {\n            try\n            {\n                IList<CacheRefreshSample> cacheRefreshSampleList = _cacheRefreshSampleList;\n                if (cacheRefreshSampleList is not null)\n                {\n                    const int MIN_TRIGGER = 10 + 4; //minimum trigger is 10 (timer interval) + 4 (additional margin for resolution delays to avoid record expiry)\n                    int cacheRefreshTrigger = _cachePrefetchTrigger < MIN_TRIGGER ? MIN_TRIGGER : _cachePrefetchTrigger;\n\n                    for (int i = 0; i < cacheRefreshSampleList.Count; i++)\n                    {\n                        CacheRefreshSample sample = cacheRefreshSampleList[i];\n                        if (sample is null)\n                            continue; //currently being refreshed\n\n                        DnsQuestionRecord neededQuestion = await GetCacheRefreshNeededQueryAsync(sample.SampleQuestion, cacheRefreshTrigger);\n                        if (neededQuestion is null)\n                            continue; //no need to refresh for this query\n\n                        //run in resolver thread pool\n                        if (_resolverTaskPool.TryQueueTask(delegate (object state)\n                            {\n                                return RefreshCacheAsync(neededQuestion, cacheRefreshSampleList, sample, (int)state);\n                            }, i)\n                        )\n                        {\n                            //refresh cache task was queued\n                            cacheRefreshSampleList[i] = null; //remove from sample list to avoid concurrent refresh attempt\n                        }\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _log.Write(ex);\n            }\n            finally\n            {\n                lock (_cachePrefetchRefreshTimerLock)\n                {\n                    _cachePrefetchRefreshTimer?.Change(CACHE_PREFETCH_REFRESH_TIMER_INTEVAL, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void ResetPrefetchTimers()\n        {\n            if ((_cachePrefetchTrigger == 0) || (_recursion == DnsServerRecursion.Deny))\n            {\n                lock (_cachePrefetchSamplingTimerLock)\n                {\n                    _cachePrefetchSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n\n                lock (_cachePrefetchRefreshTimerLock)\n                {\n                    _cachePrefetchRefreshTimer?.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n            else if (_state == ServiceState.Running)\n            {\n                lock (_cachePrefetchSamplingTimerLock)\n                {\n                    _cachePrefetchSamplingTimer?.Change(CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL, Timeout.Infinite);\n                }\n\n                lock (_cachePrefetchRefreshTimerLock)\n                {\n                    _cachePrefetchRefreshTimer?.Change(CACHE_PREFETCH_REFRESH_TIMER_INTEVAL, Timeout.Infinite);\n                }\n            }\n        }\n\n        private bool IsQpmLimitBypassed(IPAddress remoteIP)\n        {\n            if (IPAddress.IsLoopback(remoteIP))\n                return true;\n\n            if (_qpmLimitBypassList is not null)\n            {\n                foreach (NetworkAddress networkAddress in _qpmLimitBypassList)\n                {\n                    if (networkAddress.Contains(remoteIP))\n                        return true;\n                }\n            }\n\n            return false;\n        }\n\n        private bool HasQpmLimitExceeded(NetworkAddress clientSubnet, DnsTransportProtocol protocol, (int, int) qpmLimits, IReadOnlyDictionary<NetworkAddress, (long, long)> qpmLimitClientSubnetStats, out int qpmLimit, out int currentQpm)\n        {\n            qpmLimit = protocol == DnsTransportProtocol.Udp ? qpmLimits.Item1 : qpmLimits.Item2;\n\n            if ((qpmLimit > 0) && qpmLimitClientSubnetStats.TryGetValue(clientSubnet, out (long, long) countPerSampleTuple))\n            {\n                long countPerSample = protocol == DnsTransportProtocol.Udp ? countPerSampleTuple.Item1 : countPerSampleTuple.Item2;\n\n                long averageCountPerMinute = countPerSample / _qpmLimitSampleMinutes;\n                if (averageCountPerMinute >= qpmLimit)\n                {\n                    currentQpm = (int)averageCountPerMinute;\n                    return true;\n                }\n            }\n\n            currentQpm = 0;\n            return false;\n        }\n\n        internal bool HasQpmLimitExceeded(IPAddress remoteIP, DnsTransportProtocol protocol)\n        {\n            if (_qpmLimitClientSubnetStats is null)\n                return false;\n\n            if ((_qpmPrefixLimitsIPv4.Count < 1) && (_qpmPrefixLimitsIPv6.Count < 1))\n                return false;\n\n            if (IsQpmLimitBypassed(remoteIP))\n                return false;\n\n            switch (remoteIP.AddressFamily)\n            {\n                case AddressFamily.InterNetwork:\n                    foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in _qpmPrefixLimitsIPv4)\n                    {\n                        if (HasQpmLimitExceeded(new NetworkAddress(remoteIP, (byte)qpmPrefixLimit.Key), protocol, qpmPrefixLimit.Value, _qpmLimitClientSubnetStats, out _, out _))\n                            return true;\n                    }\n\n                    break;\n\n                case AddressFamily.InterNetworkV6:\n                    foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in _qpmPrefixLimitsIPv6)\n                    {\n                        if (HasQpmLimitExceeded(new NetworkAddress(remoteIP, (byte)qpmPrefixLimit.Key), protocol, qpmPrefixLimit.Value, _qpmLimitClientSubnetStats, out _, out _))\n                            return true;\n                    }\n\n                    break;\n\n                default:\n                    throw new NotSupportedException(\"AddressFamily not supported.\");\n            }\n\n            return false;\n        }\n\n        private void QpmLimitSamplingTimerCallback(object state)\n        {\n            try\n            {\n                Dictionary<NetworkAddress, (long, long)> qpmLimitClientSubnetStats = _statsManager.GetLatestClientSubnetStats(_qpmLimitSampleMinutes, _qpmPrefixLimitsIPv4.Keys, _qpmPrefixLimitsIPv6.Keys);\n\n                WriteClientSubnetRateLimitLog(_qpmLimitClientSubnetStats, qpmLimitClientSubnetStats);\n\n                _qpmLimitClientSubnetStats = qpmLimitClientSubnetStats;\n            }\n            catch (Exception ex)\n            {\n                _log.Write(ex);\n            }\n            finally\n            {\n                lock (_qpmLimitSamplingTimerLock)\n                {\n                    _qpmLimitSamplingTimer?.Change(QPM_LIMIT_SAMPLING_TIMER_INTERVAL, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void WriteClientSubnetRateLimitLog(IReadOnlyDictionary<NetworkAddress, (long, long)> oldQpmLimitClientSubnetStats, Dictionary<NetworkAddress, (long, long)> newQpmLimitClientSubnetStats)\n        {\n            if (oldQpmLimitClientSubnetStats is not null)\n            {\n                foreach (KeyValuePair<NetworkAddress, (long, long)> sampleEntry in oldQpmLimitClientSubnetStats)\n                {\n                    if (IsQpmLimitBypassed(sampleEntry.Key.GetLastAddress()))\n                        continue; //network bypassed\n\n                    IReadOnlyDictionary<int, (int, int)> qpmPrefixLimits;\n\n                    switch (sampleEntry.Key.AddressFamily)\n                    {\n                        case AddressFamily.InterNetwork:\n                            qpmPrefixLimits = _qpmPrefixLimitsIPv4;\n                            break;\n\n                        case AddressFamily.InterNetworkV6:\n                            qpmPrefixLimits = _qpmPrefixLimitsIPv6;\n                            break;\n\n                        default:\n                            continue;\n                    }\n\n                    if (qpmPrefixLimits.TryGetValue(sampleEntry.Key.PrefixLength, out (int, int) qpmPrefixLimitValue))\n                    {\n                        //for udp\n                        if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _))\n                        {\n                            //previously over limit\n                            if (!HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitUdp, out int currentQpmUdp))\n                            {\n                                //currently under limit\n                                _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.\");\n                            }\n                        }\n\n                        //for tcp\n                        if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _))\n                        {\n                            //previously over limit\n                            if (!HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitTcp, out int currentQpmTcp))\n                            {\n                                //currently under limit\n                                _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.\");\n                            }\n                        }\n                    }\n                }\n            }\n\n            foreach (KeyValuePair<NetworkAddress, (long, long)> sampleEntry in newQpmLimitClientSubnetStats)\n            {\n                if (IsQpmLimitBypassed(sampleEntry.Key.GetLastAddress()))\n                    continue; //network bypassed\n\n                IReadOnlyDictionary<int, (int, int)> qpmPrefixLimits;\n\n                switch (sampleEntry.Key.AddressFamily)\n                {\n                    case AddressFamily.InterNetwork:\n                        qpmPrefixLimits = _qpmPrefixLimitsIPv4;\n                        break;\n\n                    case AddressFamily.InterNetworkV6:\n                        qpmPrefixLimits = _qpmPrefixLimitsIPv6;\n                        break;\n\n                    default:\n                        continue;\n                }\n\n                if (qpmPrefixLimits.TryGetValue(sampleEntry.Key.PrefixLength, out (int, int) qpmPrefixLimitValue))\n                {\n                    //for udp\n                    if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitUdp, out int currentQpmUdp))\n                    {\n                        //currently over limit\n                        if ((oldQpmLimitClientSubnetStats is null) || !HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _))\n                        {\n                            //previously under limit\n                            _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.\");\n                        }\n                    }\n\n                    //for tcp\n                    if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitTcp, out int currentQpmTcp))\n                    {\n                        //currently over limit\n                        if ((oldQpmLimitClientSubnetStats is null) || !HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _))\n                        {\n                            //previously under limit\n                            _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.\");\n                        }\n                    }\n                }\n            }\n        }\n\n        private bool SendQpmLimitExceededTruncationResponse()\n        {\n            switch (_qpmLimitUdpTruncationPercentage)\n            {\n                case 0:\n                    return false;\n\n                case 100:\n                    return true;\n\n                default:\n                    int p = RandomNumberGenerator.GetInt32(100);\n                    return p < _qpmLimitUdpTruncationPercentage;\n            }\n        }\n\n        private void ResetQpsLimitTimer()\n        {\n            if ((_qpmPrefixLimitsIPv4.Count < 1) && (_qpmPrefixLimitsIPv6.Count < 1))\n            {\n                lock (_qpmLimitSamplingTimerLock)\n                {\n                    _qpmLimitSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite);\n\n                    _qpmLimitClientSubnetStats = null;\n                }\n            }\n            else if (_state == ServiceState.Running)\n            {\n                lock (_qpmLimitSamplingTimerLock)\n                {\n                    _qpmLimitSamplingTimer?.Change(0, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void UpdateThisServer()\n        {\n            foreach (IPEndPoint localEndPoint in _localEndPoints)\n            {\n                if (localEndPoint.Address.Equals(IPAddress.Any))\n                {\n                    _thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.Loopback, localEndPoint.Port));\n                    return;\n                }\n\n                if (localEndPoint.Address.Equals(IPAddress.IPv6Any))\n                {\n                    _thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.IPv6Loopback, localEndPoint.Port));\n                    return;\n                }\n            }\n\n            _thisServer = new NameServerAddress(_serverDomain, _localEndPoints[0]);\n        }\n\n        #endregion\n\n        #region resolver task pool\n\n        internal bool TryQueueResolverTask(Func<object, Task> task, object state = null)\n        {\n            return _resolverTaskPool.TryQueueTask(task, state);\n        }\n\n        private void ReconfigureResolverTaskPool(ushort maxConcurrentResolutionsPerCore)\n        {\n            TaskPool previousResolverTaskPool = _resolverTaskPool;\n\n            int maxConcurrentResolutions = Environment.ProcessorCount * maxConcurrentResolutionsPerCore;\n            int resolverQueueSize = maxConcurrentResolutions * 5 * 10; //assuming 5 qps average resolution rate for 10 sec\n            _resolverTaskPool = new TaskPool(resolverQueueSize, maxConcurrentResolutions, _resolverTaskScheduler);\n\n            previousResolverTaskPool?.Dispose(); //stop previous task pool from queuing new tasks and complete reading\n        }\n\n        #endregion\n\n        #region doh web service\n\n        private async Task StartDoHAsync(bool throwIfBindFails)\n        {\n            IReadOnlyList<IPAddress> localAddresses = WebUtilities.GetValidKestrelLocalAddresses(_localEndPoints.Convert(delegate (IPEndPoint ep) { return ep.Address; }));\n\n            try\n            {\n                WebApplicationBuilder builder = WebApplication.CreateBuilder();\n\n                builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(Path.GetDirectoryName(_dohwwwFolder))\n                {\n                    UseActivePolling = true,\n                    UsePollingFileWatcher = true\n                };\n\n                builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_dohwwwFolder)\n                {\n                    UseActivePolling = true,\n                    UsePollingFileWatcher = true\n                };\n\n                builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions)\n                {\n                    //bind to http port\n                    if (_enableDnsOverHttp)\n                    {\n                        foreach (IPAddress localAddress in localAddresses)\n                            serverOptions.Listen(localAddress, _dnsOverHttpPort);\n                    }\n\n                    //bind to https port\n                    if (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null))\n                    {\n                        foreach (IPAddress localAddress in localAddresses)\n                        {\n                            serverOptions.Listen(localAddress, _dnsOverHttpsPort, delegate (ListenOptions listenOptions)\n                            {\n                                if (_enableDnsOverHttp3)\n                                    listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;\n                                else if (IsHttp2Supported())\n                                    listenOptions.Protocols = HttpProtocols.Http1AndHttp2;\n                                else\n                                    listenOptions.Protocols = HttpProtocols.Http1;\n\n                                listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)\n                                {\n                                    return ValueTask.FromResult(_dohSslServerAuthenticationOptions);\n                                }, null);\n                            });\n                        }\n                    }\n\n                    serverOptions.AddServerHeader = false;\n                    serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout);\n                    serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout);\n                    serverOptions.Limits.MaxRequestHeadersTotalSize = 4096;\n                    serverOptions.Limits.MaxRequestLineSize = serverOptions.Limits.MaxRequestHeadersTotalSize;\n                    serverOptions.Limits.MaxRequestBufferSize = serverOptions.Limits.MaxRequestLineSize;\n                    serverOptions.Limits.MaxRequestBodySize = 64 * 1024;\n                    serverOptions.Limits.MaxResponseBufferSize = 4096;\n                });\n\n                builder.Logging.ClearProviders();\n\n                _dohWebService = builder.Build();\n\n                _dohWebService.UseDefaultFiles();\n                _dohWebService.UseStaticFiles(new StaticFileOptions()\n                {\n                    OnPrepareResponse = delegate (StaticFileResponseContext ctx)\n                    {\n                        ctx.Context.Response.Headers[\"X-Robots-Tag\"] = \"noindex, nofollow\";\n                        ctx.Context.Response.Headers.CacheControl = \"no-cache\";\n                    },\n                    ServeUnknownFileTypes = true\n                });\n\n                _dohWebService.UseRouting();\n                _dohWebService.MapGet(\"/dns-query\", ProcessDoHRequestAsync);\n                _dohWebService.MapPost(\"/dns-query\", ProcessDoHRequestAsync);\n\n                await _dohWebService.StartAsync();\n\n                foreach (IPAddress localAddress in localAddresses)\n                {\n                    if (_enableDnsOverHttp)\n                        _log.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), \"Http\", \"DNS Server was bound successfully.\");\n\n                    if (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null))\n                        _log.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), \"Https\", \"DNS Server was bound successfully.\");\n                }\n            }\n            catch (Exception ex)\n            {\n                await StopDoHAsync();\n\n                foreach (IPAddress localAddress in localAddresses)\n                {\n                    if (_enableDnsOverHttp)\n                        _log.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), \"Http\", \"DNS Server failed to bind.\");\n\n                    if (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null))\n                        _log.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), \"Https\", \"DNS Server failed to bind.\");\n                }\n\n                _log.Write(ex);\n\n                if (throwIfBindFails)\n                    throw;\n            }\n        }\n\n        private async Task StopDoHAsync()\n        {\n            if (_dohWebService is not null)\n            {\n                try\n                {\n                    await _dohWebService.DisposeAsync();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n\n                _dohWebService = null;\n            }\n        }\n\n        private bool IsHttp2Supported()\n        {\n            if (_enableDnsOverHttp3)\n                return true;\n\n            switch (Environment.OSVersion.Platform)\n            {\n                case PlatformID.Win32NT:\n                    return Environment.OSVersion.Version.Major >= 10; //http/2 supported on Windows Server 2016/Windows 10 or later\n\n                case PlatformID.Unix:\n                    return true; //http/2 supported on Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)\n\n                default:\n                    return false;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task StartAsync(bool throwIfBindFails = false)\n        {\n            if (_disposed)\n                ObjectDisposedException.ThrowIf(_disposed, this);\n\n            if (_state != ServiceState.Stopped)\n                throw new InvalidOperationException(\"DNS Server is already running.\");\n\n            _state = ServiceState.Starting;\n\n            //bind on all local end points\n            foreach (IPEndPoint localEP in _localEndPoints)\n            {\n                Socket udpListener = null;\n\n                try\n                {\n                    udpListener = new Socket(localEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);\n\n                    #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom()\n\n                    if (Environment.OSVersion.Platform == PlatformID.Win32NT)\n                    {\n                        const uint IOC_IN = 0x80000000;\n                        const uint IOC_VENDOR = 0x18000000;\n                        const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;\n\n                        udpListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);\n                    }\n\n                    #endregion\n\n                    if (Environment.OSVersion.Platform == PlatformID.Unix)\n                        udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses\n\n                    udpListener.ReceiveBufferSize = 512 * 1024;\n                    udpListener.SendBufferSize = 512 * 1024;\n\n                    try\n                    {\n                        udpListener.Bind(localEP);\n                    }\n                    catch (SocketException ex1)\n                    {\n                        switch (ex1.ErrorCode)\n                        {\n                            case 99: //SocketException (99): Cannot assign requested address\n                                await Task.Delay(5000); //wait for address to be available before retrying\n                                udpListener.Bind(localEP);\n                                break;\n\n                            default:\n                                throw;\n                        }\n                    }\n\n                    _udpListeners.Add(udpListener);\n\n                    _log.Write(localEP, DnsTransportProtocol.Udp, \"DNS Server was bound successfully.\");\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(localEP, DnsTransportProtocol.Udp, \"DNS Server failed to bind.\\r\\n\" + ex.ToString());\n\n                    udpListener?.Dispose();\n\n                    if (throwIfBindFails)\n                        throw;\n                }\n\n                if (_enableDnsOverUdpProxy)\n                {\n                    IPEndPoint udpProxyEP = new IPEndPoint(localEP.Address, _dnsOverUdpProxyPort);\n                    Socket udpProxyListener = null;\n\n                    try\n                    {\n                        udpProxyListener = new Socket(udpProxyEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);\n\n                        #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom()\n\n                        if (Environment.OSVersion.Platform == PlatformID.Win32NT)\n                        {\n                            const uint IOC_IN = 0x80000000;\n                            const uint IOC_VENDOR = 0x18000000;\n                            const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;\n\n                            udpProxyListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);\n                        }\n\n                        #endregion\n\n                        if (Environment.OSVersion.Platform == PlatformID.Unix)\n                            udpProxyListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses\n\n                        udpProxyListener.ReceiveBufferSize = 512 * 1024;\n                        udpProxyListener.SendBufferSize = 512 * 1024;\n\n                        udpProxyListener.Bind(udpProxyEP);\n\n                        _udpProxyListeners.Add(udpProxyListener);\n\n                        _log.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, \"DNS Server was bound successfully.\");\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, \"DNS Server failed to bind.\\r\\n\" + ex.ToString());\n\n                        udpProxyListener?.Dispose();\n\n                        if (throwIfBindFails)\n                            throw;\n                    }\n                }\n\n                Socket tcpListener = null;\n\n                try\n                {\n                    tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);\n\n                    if (Environment.OSVersion.Platform == PlatformID.Unix)\n                        tcpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses\n\n                    tcpListener.Bind(localEP);\n                    tcpListener.Listen(_listenBacklog);\n\n                    _tcpListeners.Add(tcpListener);\n\n                    _log.Write(localEP, DnsTransportProtocol.Tcp, \"DNS Server was bound successfully.\");\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(localEP, DnsTransportProtocol.Tcp, \"DNS Server failed to bind.\\r\\n\" + ex.ToString());\n\n                    tcpListener?.Dispose();\n\n                    if (throwIfBindFails)\n                        throw;\n                }\n\n                if (_enableDnsOverTcpProxy)\n                {\n                    IPEndPoint tcpProxyEP = new IPEndPoint(localEP.Address, _dnsOverTcpProxyPort);\n                    Socket tcpProxyListner = null;\n\n                    try\n                    {\n                        tcpProxyListner = new Socket(tcpProxyEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);\n\n                        if (Environment.OSVersion.Platform == PlatformID.Unix)\n                            tcpProxyListner.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses\n\n                        tcpProxyListner.Bind(tcpProxyEP);\n                        tcpProxyListner.Listen(_listenBacklog);\n\n                        _tcpProxyListeners.Add(tcpProxyListner);\n\n                        _log.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, \"DNS Server was bound successfully.\");\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, \"DNS Server failed to bind.\\r\\n\" + ex.ToString());\n\n                        tcpProxyListner?.Dispose();\n\n                        if (throwIfBindFails)\n                            throw;\n                    }\n                }\n\n                if (_enableDnsOverTls && (_dotSslServerAuthenticationOptions is not null))\n                {\n                    IPEndPoint tlsEP = new IPEndPoint(localEP.Address, _dnsOverTlsPort);\n                    Socket tlsListener = null;\n\n                    try\n                    {\n                        tlsListener = new Socket(tlsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);\n\n                        if (Environment.OSVersion.Platform == PlatformID.Unix)\n                            tlsListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses\n\n                        tlsListener.Bind(tlsEP);\n                        tlsListener.Listen(_listenBacklog);\n\n                        _tlsListeners.Add(tlsListener);\n\n                        _log.Write(tlsEP, DnsTransportProtocol.Tls, \"DNS Server was bound successfully.\");\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(tlsEP, DnsTransportProtocol.Tls, \"DNS Server failed to bind.\\r\\n\" + ex.ToString());\n\n                        tlsListener?.Dispose();\n\n                        if (throwIfBindFails)\n                            throw;\n                    }\n                }\n\n                if (_enableDnsOverQuic && (_doqSslServerAuthenticationOptions is not null))\n                {\n                    IPEndPoint quicEP = new IPEndPoint(localEP.Address, _dnsOverQuicPort);\n                    QuicListener quicListener = null;\n\n                    try\n                    {\n                        QuicListenerOptions listenerOptions = new QuicListenerOptions()\n                        {\n                            ListenEndPoint = quicEP,\n                            ListenBacklog = _listenBacklog,\n                            ApplicationProtocols = _doqApplicationProtocols,\n                            ConnectionOptionsCallback = delegate (QuicConnection quicConnection, SslClientHelloInfo sslClientHello, CancellationToken cancellationToken)\n                            {\n                                QuicServerConnectionOptions serverConnectionOptions = new QuicServerConnectionOptions()\n                                {\n                                    DefaultCloseErrorCode = (long)DnsOverQuicErrorCodes.DOQ_NO_ERROR,\n                                    DefaultStreamErrorCode = (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR,\n                                    MaxInboundUnidirectionalStreams = 0,\n                                    MaxInboundBidirectionalStreams = _quicMaxInboundStreams,\n                                    IdleTimeout = TimeSpan.FromMilliseconds(_quicIdleTimeout),\n                                    ServerAuthenticationOptions = _doqSslServerAuthenticationOptions\n                                };\n\n                                return ValueTask.FromResult(serverConnectionOptions);\n                            }\n                        };\n\n                        quicListener = await QuicListener.ListenAsync(listenerOptions);\n\n                        _quicListeners.Add(quicListener);\n\n                        _log.Write(quicEP, DnsTransportProtocol.Quic, \"DNS Server was bound successfully.\");\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(quicEP, DnsTransportProtocol.Quic, \"DNS Server failed to bind.\\r\\n\" + ex.ToString());\n\n                        if (quicListener is not null)\n                            await quicListener.DisposeAsync();\n\n                        if (throwIfBindFails)\n                            throw;\n                    }\n                }\n            }\n\n            //start reading query packets\n            int listenerTaskCount = Environment.ProcessorCount;\n\n            foreach (Socket udpListener in _udpListeners)\n            {\n                for (int i = 0; i < listenerTaskCount; i++)\n                {\n                    _ = Task.Factory.StartNew(delegate ()\n                    {\n                        return ReadUdpRequestAsync(udpListener, DnsTransportProtocol.Udp);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);\n                }\n            }\n\n            foreach (Socket udpProxyListener in _udpProxyListeners)\n            {\n                for (int i = 0; i < listenerTaskCount; i++)\n                {\n                    _ = Task.Factory.StartNew(delegate ()\n                    {\n                        return ReadUdpRequestAsync(udpProxyListener, DnsTransportProtocol.UdpProxy);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);\n                }\n            }\n\n            foreach (Socket tcpListener in _tcpListeners)\n            {\n                for (int i = 0; i < listenerTaskCount; i++)\n                {\n                    _ = Task.Factory.StartNew(delegate ()\n                    {\n                        return AcceptConnectionAsync(tcpListener, DnsTransportProtocol.Tcp);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);\n                }\n            }\n\n            foreach (Socket tcpProxyListener in _tcpProxyListeners)\n            {\n                for (int i = 0; i < listenerTaskCount; i++)\n                {\n                    _ = Task.Factory.StartNew(delegate ()\n                    {\n                        return AcceptConnectionAsync(tcpProxyListener, DnsTransportProtocol.TcpProxy);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);\n                }\n            }\n\n            foreach (Socket tlsListener in _tlsListeners)\n            {\n                for (int i = 0; i < listenerTaskCount; i++)\n                {\n                    _ = Task.Factory.StartNew(delegate ()\n                    {\n                        return AcceptConnectionAsync(tlsListener, DnsTransportProtocol.Tls);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);\n                }\n            }\n\n            foreach (QuicListener quicListener in _quicListeners)\n            {\n                for (int i = 0; i < listenerTaskCount; i++)\n                {\n                    _ = Task.Factory.StartNew(delegate ()\n                    {\n                        return AcceptQuicConnectionAsync(quicListener);\n                    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler);\n                }\n            }\n\n            if (_enableDnsOverHttp || (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null)))\n                await StartDoHAsync(throwIfBindFails);\n\n            _cachePrefetchSamplingTimer = new Timer(CachePrefetchSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n            _cachePrefetchRefreshTimer = new Timer(CachePrefetchRefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n            _qpmLimitSamplingTimer = new Timer(QpmLimitSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n\n            _state = ServiceState.Running;\n\n            UpdateThisServer();\n            ResetPrefetchTimers();\n            ResetQpsLimitTimer();\n        }\n\n        public async Task StopAsync()\n        {\n            if (_state != ServiceState.Running)\n                return;\n\n            _state = ServiceState.Stopping;\n\n            lock (_cachePrefetchSamplingTimerLock)\n            {\n                if (_cachePrefetchSamplingTimer is not null)\n                {\n                    _cachePrefetchSamplingTimer.Dispose();\n                    _cachePrefetchSamplingTimer = null;\n                }\n            }\n\n            lock (_cachePrefetchRefreshTimerLock)\n            {\n                if (_cachePrefetchRefreshTimer is not null)\n                {\n                    _cachePrefetchRefreshTimer.Dispose();\n                    _cachePrefetchRefreshTimer = null;\n                }\n            }\n\n            lock (_qpmLimitSamplingTimerLock)\n            {\n                if (_qpmLimitSamplingTimer is not null)\n                {\n                    _qpmLimitSamplingTimer.Dispose();\n                    _qpmLimitSamplingTimer = null;\n                }\n            }\n\n            foreach (Socket udpListener in _udpListeners)\n            {\n                try\n                {\n                    udpListener.Dispose();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n            }\n\n            foreach (Socket udpProxyListener in _udpProxyListeners)\n            {\n                try\n                {\n                    udpProxyListener.Dispose();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n            }\n\n            foreach (Socket tcpListener in _tcpListeners)\n            {\n                try\n                {\n                    tcpListener.Dispose();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n            }\n\n            foreach (Socket tcpProxyListener in _tcpProxyListeners)\n            {\n                try\n                {\n                    tcpProxyListener.Dispose();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n            }\n\n            foreach (Socket tlsListener in _tlsListeners)\n            {\n                try\n                {\n                    tlsListener.Dispose();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n            }\n\n            foreach (QuicListener quicListener in _quicListeners)\n            {\n                try\n                {\n                    await quicListener.DisposeAsync();\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(ex);\n                }\n            }\n\n            _udpListeners.Clear();\n            _udpProxyListeners.Clear();\n            _tcpListeners.Clear();\n            _tcpProxyListeners.Clear();\n            _tlsListeners.Clear();\n            _quicListeners.Clear();\n\n            await StopDoHAsync();\n\n            _state = ServiceState.Stopped;\n        }\n\n        public Task<DnsDatagram> DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false, CancellationToken cancellationToken = default)\n        {\n            return DirectQueryAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, [question]), timeout, skipDnsAppAuthoritativeRequestHandlers, cancellationToken);\n        }\n\n        public Task<DnsDatagram> DirectQueryAsync(DnsDatagram request, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false, CancellationToken cancellationToken = default)\n        {\n            return TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n            {\n                return ProcessQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, timeout, null);\n            }, timeout, cancellationToken);\n        }\n\n        Task<DnsDatagram> IDnsClient.ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken)\n        {\n            return DirectQueryAsync(question, cancellationToken: cancellationToken);\n        }\n\n        #endregion\n\n        #region properties\n\n        public string ServerDomain\n        {\n            get { return _serverDomain; }\n            set\n            {\n                if (!_serverDomain.Equals(value))\n                {\n                    if (DnsClient.IsDomainNameUnicode(value))\n                        value = DnsClient.ConvertDomainNameToAscii(value);\n\n                    DnsClient.IsDomainNameValid(value, true);\n\n                    if (IPAddress.TryParse(value, out _))\n                        throw new DnsServerException(\"Invalid domain name [\" + value + \"]: IP address cannot be used for DNS server domain name.\");\n\n                    _serverDomain = value.ToLowerInvariant();\n                    _fallbackResponsiblePerson = new MailAddress(\"hostadmin@\" + _serverDomain);\n\n                    _authZoneManager.TriggerUpdateServerDomain();\n                    _allowedZoneManager.UpdateServerDomain();\n                    _blockedZoneManager.UpdateServerDomain();\n                    _blockListZoneManager.UpdateServerDomain();\n\n                    UpdateThisServer();\n                }\n            }\n        }\n\n        public string ConfigFolder\n        { get { return _configFolder; } }\n\n        public IReadOnlyList<IPEndPoint> LocalEndPoints\n        {\n            get { return _localEndPoints; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                {\n                    _localEndPoints = [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)];\n                }\n                else\n                {\n                    foreach (IPEndPoint ep in value)\n                    {\n                        if (ep.Port == 853)\n                            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));\n                    }\n\n                    _localEndPoints = value;\n                }\n            }\n        }\n\n        public LogManager LogManager\n        { get { return _log; } }\n\n        internal MailAddress DefaultResponsiblePerson\n        {\n            get { return _defaultResponsiblePerson; }\n            set { _defaultResponsiblePerson = value; }\n        }\n\n        public MailAddress ResponsiblePerson\n        {\n            get\n            {\n                if (_defaultResponsiblePerson is not null)\n                    return _defaultResponsiblePerson;\n\n                if (_fallbackResponsiblePerson is null)\n                    _fallbackResponsiblePerson = new MailAddress(\"hostadmin@\" + _serverDomain);\n\n                return _fallbackResponsiblePerson;\n            }\n        }\n\n        public NameServerAddress ThisServer\n        { get { return _thisServer; } }\n\n        public AuthZoneManager AuthZoneManager\n        { get { return _authZoneManager; } }\n\n        public AllowedZoneManager AllowedZoneManager\n        { get { return _allowedZoneManager; } }\n\n        public BlockedZoneManager BlockedZoneManager\n        { get { return _blockedZoneManager; } }\n\n        public BlockListZoneManager BlockListZoneManager\n        { get { return _blockListZoneManager; } }\n\n        public CacheZoneManager CacheZoneManager\n        { get { return _cacheZoneManager; } }\n\n        public DnsApplicationManager DnsApplicationManager\n        { get { return _dnsApplicationManager; } }\n\n        public IDnsCache DnsCache\n        { get { return _dnsCache; } }\n\n        public StatsManager StatsManager\n        { get { return _statsManager; } }\n\n        public IReadOnlyCollection<NetworkAddress> ZoneTransferAllowedNetworks\n        {\n            get { return _zoneTransferAllowedNetworks; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _zoneTransferAllowedNetworks = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(ZoneTransferAllowedNetworks), \"Networks cannot have more than 255 entries.\");\n                else\n                    _zoneTransferAllowedNetworks = value;\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAddress> NotifyAllowedNetworks\n        {\n            get { return _notifyAllowedNetworks; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _notifyAllowedNetworks = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(NotifyAllowedNetworks), \"Networks cannot have more than 255 entries.\");\n                else\n                    _notifyAllowedNetworks = value;\n            }\n        }\n\n        public bool PreferIPv6\n        {\n            get { return _preferIPv6; }\n            set\n            {\n                if (_preferIPv6 != value)\n                {\n                    _preferIPv6 = value;\n\n                    //init udp socket pool async for port randomization\n                    ThreadPool.QueueUserWorkItem(delegate (object state)\n                    {\n                        try\n                        {\n                            if (_enableUdpSocketPool)\n                                UdpClientConnection.CreateSocketPool(_preferIPv6);\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(ex);\n                        }\n                    });\n                }\n            }\n        }\n\n        public bool EnableUdpSocketPool\n        {\n            get { return _enableUdpSocketPool; }\n            set\n            {\n                if (_enableUdpSocketPool != value)\n                {\n                    _enableUdpSocketPool = value;\n\n                    //init udp socket pool async for port randomization\n                    ThreadPool.QueueUserWorkItem(delegate (object state)\n                    {\n                        try\n                        {\n                            if (_enableUdpSocketPool)\n                                UdpClientConnection.CreateSocketPool(_preferIPv6);\n                            else\n                                UdpClientConnection.DisposeSocketPool();\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(ex);\n                        }\n                    });\n                }\n            }\n        }\n\n        public ushort UdpPayloadSize\n        {\n            get { return _udpPayloadSize; }\n            set\n            {\n                if ((value < 512) || (value > 4096))\n                    throw new ArgumentOutOfRangeException(nameof(UdpPayloadSize), \"Invalid EDNS UDP payload size: valid range is 512-4096 bytes.\");\n\n                _udpPayloadSize = value;\n            }\n        }\n\n        public bool DnssecValidation\n        {\n            get { return _dnssecValidation; }\n            set\n            {\n                if (_dnssecValidation != value)\n                {\n                    if (!_dnssecValidation)\n                        _cacheZoneManager.Flush(); //flush cache to remove non validated data\n\n                    _dnssecValidation = value;\n                }\n            }\n        }\n\n        public bool EDnsClientSubnet\n        {\n            get { return _eDnsClientSubnet; }\n            set\n            {\n                if (_eDnsClientSubnet != value)\n                {\n                    _eDnsClientSubnet = value;\n\n                    if (!_eDnsClientSubnet)\n                    {\n                        ThreadPool.QueueUserWorkItem(delegate (object state)\n                        {\n                            try\n                            {\n                                _cacheZoneManager.DeleteEDnsClientSubnetData();\n                            }\n                            catch (Exception ex)\n                            {\n                                _log.Write(ex);\n                            }\n                        });\n                    }\n                }\n            }\n        }\n\n        public byte EDnsClientSubnetIPv4PrefixLength\n        {\n            get { return _eDnsClientSubnetIPv4PrefixLength; }\n            set\n            {\n                if (value > 32)\n                    throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv4PrefixLength), \"EDNS Client Subnet IPv4 prefix length cannot be greater than 32.\");\n\n                _eDnsClientSubnetIPv4PrefixLength = value;\n            }\n        }\n\n        public byte EDnsClientSubnetIPv6PrefixLength\n        {\n            get { return _eDnsClientSubnetIPv6PrefixLength; }\n            set\n            {\n                if (value > 64)\n                    throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv6PrefixLength), \"EDNS Client Subnet IPv6 prefix length cannot be greater than 64.\");\n\n                _eDnsClientSubnetIPv6PrefixLength = value;\n            }\n        }\n\n        public NetworkAddress EDnsClientSubnetIpv4Override\n        {\n            get { return _eDnsClientSubnetIpv4Override; }\n            set\n            {\n                if (value is not null)\n                {\n                    if (value.AddressFamily != AddressFamily.InterNetwork)\n                        throw new ArgumentException(\"EDNS Client Subnet IPv4 Override must be an IPv4 network address.\", nameof(EDnsClientSubnetIpv4Override));\n\n                    if (value.IsHostAddress)\n                        value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv4PrefixLength);\n                }\n\n                _eDnsClientSubnetIpv4Override = value;\n            }\n        }\n\n        public NetworkAddress EDnsClientSubnetIpv6Override\n        {\n            get { return _eDnsClientSubnetIpv6Override; }\n            set\n            {\n                if (value is not null)\n                {\n                    if (value.AddressFamily != AddressFamily.InterNetworkV6)\n                        throw new ArgumentException(\"EDNS Client Subnet IPv6 Override must be an IPv6 network address.\", nameof(EDnsClientSubnetIpv6Override));\n\n                    if (value.IsHostAddress)\n                        value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv6PrefixLength);\n                }\n\n                _eDnsClientSubnetIpv6Override = value;\n            }\n        }\n\n        public IReadOnlyDictionary<int, (int, int)> QpmPrefixLimitsIPv4\n        {\n            get { return _qpmPrefixLimitsIPv4; }\n            set\n            {\n                if (value is null)\n                {\n                    _qpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>();\n                }\n                else if (value.Count > byte.MaxValue)\n                {\n                    throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv4), \"QPM Prefix Limits for IPv4 cannot have more than 255 entries.\");\n                }\n                else\n                {\n                    foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in value)\n                    {\n                        if ((qpmPrefixLimit.Key < 0) || (qpmPrefixLimit.Key > 32))\n                            throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv4), \"QPM limit IPv4 prefix valid range is between 0 and 32.\");\n\n                        if ((qpmPrefixLimit.Value.Item1 < 0) || (qpmPrefixLimit.Value.Item2 < 0))\n                            throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv4), \"QPM limit value cannot be less than 0.\");\n                    }\n\n                    _qpmPrefixLimitsIPv4 = value;\n                }\n            }\n        }\n\n        public IReadOnlyDictionary<int, (int, int)> QpmPrefixLimitsIPv6\n        {\n            get { return _qpmPrefixLimitsIPv6; }\n            set\n            {\n                if (value is null)\n                {\n                    _qpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>();\n                }\n                else if (value.Count > byte.MaxValue)\n                {\n                    throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv6), \"QPM Prefix Limits for IPv6 cannot have more than 255 entries.\");\n                }\n                else\n                {\n                    foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in value)\n                    {\n                        if ((qpmPrefixLimit.Key < 0) || (qpmPrefixLimit.Key > 128))\n                            throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv6), \"QPM limit IPv6 prefix valid range is between 0 and 128.\");\n\n                        if ((qpmPrefixLimit.Value.Item1 < 0) || (qpmPrefixLimit.Value.Item2 < 0))\n                            throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv6), \"QPM limit value cannot be less than 0.\");\n                    }\n\n                    _qpmPrefixLimitsIPv6 = value;\n                }\n            }\n        }\n\n        public int QpmLimitSampleMinutes\n        {\n            get { return _qpmLimitSampleMinutes; }\n            set\n            {\n                if ((value < 1) || (value > 60))\n                    throw new ArgumentOutOfRangeException(nameof(QpmLimitSampleMinutes), \"Valid range is between 1 and 60 minutes.\");\n\n                _qpmLimitSampleMinutes = value;\n            }\n        }\n\n        public int QpmLimitUdpTruncationPercentage\n        {\n            get { return _qpmLimitUdpTruncationPercentage; }\n            set\n            {\n                if ((value < 0) || (value > 100))\n                    throw new ArgumentOutOfRangeException(nameof(QpmLimitUdpTruncationPercentage), \"Percentage value valid range is between 0 and 100.\");\n\n                _qpmLimitUdpTruncationPercentage = value;\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAddress> QpmLimitBypassList\n        {\n            get { return _qpmLimitBypassList; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _qpmLimitBypassList = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(QpmLimitBypassList), \"Networks cannot have more than 255 entries.\");\n                else\n                    _qpmLimitBypassList = value;\n            }\n        }\n\n        public int ClientTimeout\n        {\n            get { return _clientTimeout; }\n            set\n            {\n                if ((value < 1000) || (value > 10000))\n                    throw new ArgumentOutOfRangeException(nameof(ClientTimeout), \"Valid range is from 1000 to 10000.\");\n\n                _clientTimeout = value;\n            }\n        }\n\n        public int TcpSendTimeout\n        {\n            get { return _tcpSendTimeout; }\n            set\n            {\n                if ((value < 1000) || (value > 90000))\n                    throw new ArgumentOutOfRangeException(nameof(TcpSendTimeout), \"Valid range is from 1000 to 90000.\");\n\n                _tcpSendTimeout = value;\n            }\n        }\n\n        public int TcpReceiveTimeout\n        {\n            get { return _tcpReceiveTimeout; }\n            set\n            {\n                if ((value < 1000) || (value > 90000))\n                    throw new ArgumentOutOfRangeException(nameof(TcpReceiveTimeout), \"Valid range is from 1000 to 90000.\");\n\n                _tcpReceiveTimeout = value;\n            }\n        }\n\n        public int QuicIdleTimeout\n        {\n            get { return _quicIdleTimeout; }\n            set\n            {\n                if ((value < 1000) || (value > 90000))\n                    throw new ArgumentOutOfRangeException(nameof(QuicIdleTimeout), \"Valid range is from 1000 to 90000.\");\n\n                _quicIdleTimeout = value;\n            }\n        }\n\n        public int QuicMaxInboundStreams\n        {\n            get { return _quicMaxInboundStreams; }\n            set\n            {\n                if ((value < 0) || (value > 1000))\n                    throw new ArgumentOutOfRangeException(nameof(QuicMaxInboundStreams), \"Valid range is from 1 to 1000.\");\n\n                _quicMaxInboundStreams = value;\n            }\n        }\n\n        public int ListenBacklog\n        {\n            get { return _listenBacklog; }\n            set { _listenBacklog = value; }\n        }\n\n        public ushort MaxConcurrentResolutionsPerCore\n        {\n            get { return Convert.ToUInt16(_resolverTaskPool.MaximumConcurrencyLevel / Environment.ProcessorCount); }\n            set\n            {\n                if (value < 1)\n                    throw new ArgumentOutOfRangeException(nameof(MaxConcurrentResolutionsPerCore), \"Value cannot be less than 1.\");\n\n                if (MaxConcurrentResolutionsPerCore != value)\n                    ReconfigureResolverTaskPool(value);\n            }\n        }\n\n        public bool EnableDnsOverUdpProxy\n        {\n            get { return _enableDnsOverUdpProxy; }\n            set { _enableDnsOverUdpProxy = value; }\n        }\n\n        public bool EnableDnsOverTcpProxy\n        {\n            get { return _enableDnsOverTcpProxy; }\n            set { _enableDnsOverTcpProxy = value; }\n        }\n\n        public bool EnableDnsOverHttp\n        {\n            get { return _enableDnsOverHttp; }\n            set { _enableDnsOverHttp = value; }\n        }\n\n        public bool EnableDnsOverTls\n        {\n            get { return _enableDnsOverTls; }\n            set { _enableDnsOverTls = value; }\n        }\n\n        public bool EnableDnsOverHttps\n        {\n            get { return _enableDnsOverHttps; }\n            set { _enableDnsOverHttps = value; }\n        }\n\n        public bool EnableDnsOverHttp3\n        {\n            get { return _enableDnsOverHttp3; }\n            set { _enableDnsOverHttp3 = value; }\n        }\n\n        public bool EnableDnsOverQuic\n        {\n            get { return _enableDnsOverQuic; }\n            set { _enableDnsOverQuic = value; }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> ReverseProxyNetworkACL\n        {\n            get { return _reverseProxyNetworkACL; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _reverseProxyNetworkACL = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(ReverseProxyNetworkACL), \"Network Access Control List cannot have more than 255 entries.\");\n                else\n                    _reverseProxyNetworkACL = value;\n            }\n        }\n\n        public int DnsOverUdpProxyPort\n        {\n            get { return _dnsOverUdpProxyPort; }\n            set\n            {\n                if ((value < ushort.MinValue) || (value > ushort.MaxValue))\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverUdpProxyPort), \"Port number valid range is from 0 to 65535.\");\n\n                _dnsOverUdpProxyPort = value;\n            }\n        }\n\n        public int DnsOverTcpProxyPort\n        {\n            get { return _dnsOverTcpProxyPort; }\n            set\n            {\n                if ((value < ushort.MinValue) || (value > ushort.MaxValue))\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverTcpProxyPort), \"Port number valid range is from 0 to 65535.\");\n\n                _dnsOverTcpProxyPort = value;\n            }\n        }\n\n        public int DnsOverHttpPort\n        {\n            get { return _dnsOverHttpPort; }\n            set\n            {\n                if ((value < ushort.MinValue) || (value > ushort.MaxValue))\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), \"Port number valid range is from 0 to 65535.\");\n\n                if (value == 53)\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), \"Port 53 cannot be used for DNS-over-HTTP service. Please use a different port.\");\n\n                if (value == 853)\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), \"Port 853 is reserved for DNS-over-TLS service. Please use a different port for DNS-over-HTTP service.\");\n\n                _dnsOverHttpPort = value;\n            }\n        }\n\n        public int DnsOverTlsPort\n        {\n            get { return _dnsOverTlsPort; }\n            set\n            {\n                if ((value < ushort.MinValue) || (value > ushort.MaxValue))\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverTlsPort), \"Port number valid range is from 0 to 65535.\");\n\n                if (value == 53)\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverTlsPort), \"Port 53 cannot be used for DNS-over-TLS service. Please use a different port.\");\n\n                _dnsOverTlsPort = value;\n            }\n        }\n\n        public int DnsOverHttpsPort\n        {\n            get { return _dnsOverHttpsPort; }\n            set\n            {\n                if ((value < ushort.MinValue) || (value > ushort.MaxValue))\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), \"Port number valid range is from 0 to 65535.\");\n\n                if (value == 53)\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), \"Port 53 cannot be used for DNS-over-HTTPS service. Please use a different port.\");\n\n                if (value == 853)\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), \"Port 853 is reserved for DNS-over-TLS service. Please use a different port for DNS-over-HTTPS service.\");\n\n                _dnsOverHttpsPort = value;\n            }\n        }\n\n        public int DnsOverQuicPort\n        {\n            get { return _dnsOverQuicPort; }\n            set\n            {\n                if ((value < ushort.MinValue) || (value > ushort.MaxValue))\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverQuicPort), \"Port number valid range is from 0 to 65535.\");\n\n                if (value == 53)\n                    throw new ArgumentOutOfRangeException(nameof(DnsOverQuicPort), \"Port 53 cannot be used for DNS-over-QUIC service. Please use a different port.\");\n\n                _dnsOverQuicPort = value;\n            }\n        }\n\n        public string DnsTlsCertificatePath\n        { get { return _dnsTlsCertificatePath; } }\n\n        public string DnsTlsCertificatePassword\n        { get { return _dnsTlsCertificatePassword; } }\n\n        public string DnsOverHttpRealIpHeader\n        {\n            get { return _dnsOverHttpRealIpHeader; }\n            set\n            {\n                if (string.IsNullOrEmpty(value))\n                    _dnsOverHttpRealIpHeader = \"X-Real-IP\";\n                else if (value.Length > 255)\n                    throw new ArgumentException(\"DNS-over-HTTP Real IP header name cannot exceed 255 characters.\", nameof(DnsOverHttpRealIpHeader));\n                else if (value.Contains(' '))\n                    throw new ArgumentException(\"DNS-over-HTTP Real IP header name cannot contain invalid characters.\", nameof(DnsOverHttpRealIpHeader));\n                else\n                    _dnsOverHttpRealIpHeader = value;\n            }\n        }\n\n        public IReadOnlyDictionary<string, TsigKey> TsigKeys\n        {\n            get { return _tsigKeys; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _tsigKeys = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(TsigKeys), \"TSIG keys cannot have more than 255 entries.\");\n                else\n                    _tsigKeys = value;\n            }\n        }\n\n        public DnsServerRecursion Recursion\n        {\n            get { return _recursion; }\n            set\n            {\n                if (_recursion != value)\n                {\n                    if ((_recursion == DnsServerRecursion.Deny) || (value == DnsServerRecursion.Deny))\n                    {\n                        _recursion = value;\n                        ResetPrefetchTimers();\n                    }\n                    else\n                    {\n                        _recursion = value;\n                    }\n                }\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> RecursionNetworkACL\n        {\n            get { return _recursionNetworkACL; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _recursionNetworkACL = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(RecursionNetworkACL), \"Network Access Control List cannot have more than 255 entries.\");\n                else\n                    _recursionNetworkACL = value;\n            }\n        }\n\n        public bool RandomizeName\n        {\n            get { return _randomizeName; }\n            set { _randomizeName = value; }\n        }\n\n        public bool QnameMinimization\n        {\n            get { return _qnameMinimization; }\n            set { _qnameMinimization = value; }\n        }\n\n        public int ResolverRetries\n        {\n            get { return _resolverRetries; }\n            set\n            {\n                if ((value < 1) || (value > 10))\n                    throw new ArgumentOutOfRangeException(nameof(ResolverRetries), \"Valid range is from 1 to 10.\");\n\n                _resolverRetries = value;\n            }\n        }\n\n        public int ResolverTimeout\n        {\n            get { return _resolverTimeout; }\n            set\n            {\n                if ((value < 1000) || (value > 10000))\n                    throw new ArgumentOutOfRangeException(nameof(ResolverTimeout), \"Valid range is from 1000 to 10000.\");\n\n                _resolverTimeout = value;\n            }\n        }\n\n        public int ResolverConcurrency\n        {\n            get { return _resolverConcurrency; }\n            set\n            {\n                if ((value < 1) || (value > 4))\n                    throw new ArgumentOutOfRangeException(nameof(ResolverConcurrency), \"Valid range is from 1 to 4.\");\n\n                _resolverConcurrency = value;\n            }\n        }\n\n        public int ResolverMaxStackCount\n        {\n            get { return _resolverMaxStackCount; }\n            set\n            {\n                if ((value < 10) || (value > 30))\n                    throw new ArgumentOutOfRangeException(nameof(ResolverMaxStackCount), \"Valid range is from 10 to 30.\");\n\n                _resolverMaxStackCount = value;\n            }\n        }\n\n        public bool SaveCacheToDisk\n        {\n            get { return _saveCacheToDisk; }\n            set\n            {\n                _saveCacheToDisk = value;\n\n                if (!_saveCacheToDisk)\n                {\n                    try\n                    {\n                        _cacheZoneManager.DeleteCacheZoneFile();\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(ex);\n                    }\n                }\n            }\n        }\n\n        public bool ServeStale\n        {\n            get { return _serveStale; }\n            set { _serveStale = value; }\n        }\n\n        public int ServeStaleMaxWaitTime\n        {\n            get { return _serveStaleMaxWaitTime; }\n            set\n            {\n                if ((value < 0) || (value > 1800))\n                    throw new ArgumentOutOfRangeException(nameof(ServeStaleMaxWaitTime), \"Serve stale max wait time valid range is 0 to 1800 milliseconds. Default value is 1800 milliseconds.\");\n\n                _serveStaleMaxWaitTime = value;\n            }\n        }\n\n        public int CachePrefetchEligibility\n        {\n            get { return _cachePrefetchEligibility; }\n            set\n            {\n                if (value < 2)\n                    throw new ArgumentOutOfRangeException(nameof(CachePrefetchEligibility), \"Valid value is greater that or equal to 2.\");\n\n                _cachePrefetchEligibility = value;\n            }\n        }\n\n        public int CachePrefetchTrigger\n        {\n            get { return _cachePrefetchTrigger; }\n            set\n            {\n                if (value < 0)\n                    throw new ArgumentOutOfRangeException(nameof(CachePrefetchTrigger), \"Valid value is greater that or equal to 0.\");\n\n                if (_cachePrefetchTrigger != value)\n                {\n                    if ((_cachePrefetchTrigger == 0) || (value == 0))\n                    {\n                        _cachePrefetchTrigger = value;\n                        ResetPrefetchTimers();\n                    }\n                    else\n                    {\n                        _cachePrefetchTrigger = value;\n                    }\n                }\n            }\n        }\n\n        public int CachePrefetchSampleIntervalMinutes\n        {\n            get { return _cachePrefetchSampleIntervalMinutes; }\n            set\n            {\n                if ((value < 1) || (value > 60))\n                    throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleIntervalMinutes), \"Valid range is between 1 and 60 minutes.\");\n\n                if (_cachePrefetchSampleIntervalMinutes != value)\n                {\n                    _cachePrefetchSampleIntervalMinutes = value;\n                    ResetPrefetchTimers();\n                }\n            }\n        }\n\n        public int CachePrefetchSampleEligibilityHitsPerHour\n        {\n            get { return _cachePrefetchSampleEligibilityHitsPerHour; }\n            set\n            {\n                if (value < 1)\n                    throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleEligibilityHitsPerHour), \"Valid value is greater than or equal to 1.\");\n\n                _cachePrefetchSampleEligibilityHitsPerHour = value;\n            }\n        }\n\n        public bool EnableBlocking\n        {\n            get { return _enableBlocking; }\n            set\n            {\n                _enableBlocking = value;\n\n                if (_enableBlocking)\n                    _blockListZoneManager.StopTemporaryDisableBlockingTimer();\n            }\n        }\n\n        public bool AllowTxtBlockingReport\n        {\n            get { return _allowTxtBlockingReport; }\n            set { _allowTxtBlockingReport = value; }\n        }\n\n        public IReadOnlyCollection<NetworkAddress> BlockingBypassList\n        {\n            get { return _blockingBypassList; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _blockingBypassList = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(BlockingBypassList), \"Networks cannot have more than 255 entries.\");\n                else\n                    _blockingBypassList = value;\n            }\n        }\n\n        public DnsServerBlockingType BlockingType\n        {\n            get { return _blockingType; }\n            set { _blockingType = value; }\n        }\n\n        public uint BlockingAnswerTtl\n        {\n            get { return _blockingAnswerTtl; }\n            set\n            {\n                if (_blockingAnswerTtl != value)\n                {\n                    _blockingAnswerTtl = value;\n\n                    //update SOA MINIMUM values\n                    _blockedZoneManager.UpdateServerDomain();\n                    _blockListZoneManager.UpdateServerDomain();\n                }\n            }\n        }\n\n        public IReadOnlyCollection<DnsARecordData> CustomBlockingARecords\n        {\n            get { return _customBlockingARecords; }\n            set\n            {\n                if (value is null)\n                    value = [];\n\n                _customBlockingARecords = value;\n            }\n        }\n\n        public IReadOnlyCollection<DnsAAAARecordData> CustomBlockingAAAARecords\n        {\n            get { return _customBlockingAAAARecords; }\n            set\n            {\n                if (value is null)\n                    value = [];\n\n                _customBlockingAAAARecords = value;\n            }\n        }\n\n        public NetProxy Proxy\n        {\n            get { return _proxy; }\n            set { _proxy = value; }\n        }\n\n        public IReadOnlyList<NameServerAddress> Forwarders\n        {\n            get { return _forwarders; }\n            set { _forwarders = value; }\n        }\n\n        public bool ConcurrentForwarding\n        {\n            get { return _concurrentForwarding; }\n            set { _concurrentForwarding = value; }\n        }\n\n        public int ForwarderRetries\n        {\n            get { return _forwarderRetries; }\n            set\n            {\n                if ((value < 1) || (value > 10))\n                    throw new ArgumentOutOfRangeException(nameof(ForwarderRetries), \"Valid range is from 1 to 10.\");\n\n                _forwarderRetries = value;\n            }\n        }\n\n        public int ForwarderTimeout\n        {\n            get { return _forwarderTimeout; }\n            set\n            {\n                if ((value < 1000) || (value > 10000))\n                    throw new ArgumentOutOfRangeException(nameof(ForwarderTimeout), \"Valid range is from 1000 to 10000.\");\n\n                _forwarderTimeout = value;\n            }\n        }\n\n        public int ForwarderConcurrency\n        {\n            get { return _forwarderConcurrency; }\n            set\n            {\n                if ((value < 1) || (value > 10))\n                    throw new ArgumentOutOfRangeException(nameof(ForwarderConcurrency), \"Valid range is from 1 to 10.\");\n\n                _forwarderConcurrency = value;\n            }\n        }\n\n        public LogManager ResolverLogManager\n        {\n            get { return _resolverLog; }\n            set { _resolverLog = value; }\n        }\n\n        public LogManager QueryLogManager\n        {\n            get { return _queryLog; }\n            set { _queryLog = value; }\n        }\n\n        #endregion\n\n        class CacheRefreshSample\n        {\n            public CacheRefreshSample(DnsQuestionRecord sampleQuestion, IReadOnlyList<DnsResourceRecord> conditionalForwarders)\n            {\n                SampleQuestion = sampleQuestion;\n                ConditionalForwarders = conditionalForwarders;\n            }\n\n            public DnsQuestionRecord SampleQuestion { get; }\n\n            public IReadOnlyList<DnsResourceRecord> ConditionalForwarders { get; }\n        }\n\n        class RecursiveResolveResponse\n        {\n            public RecursiveResolveResponse(DnsDatagram response, DnsDatagram checkingDisabledResponse)\n            {\n                Response = response;\n                CheckingDisabledResponse = checkingDisabledResponse;\n            }\n\n            public DnsDatagram Response { get; }\n\n            public DnsDatagram CheckingDisabledResponse { get; }\n        }\n    }\n\n#pragma warning restore CA2252 // This API requires opting into preview features\n#pragma warning restore CA1416 // Validate platform compatibility\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/DnsServerException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore.Dns\n{\n    public class DnsServerException : Exception\n    {\n        #region constructors\n\n        public DnsServerException()\n            : base()\n        { }\n\n        public DnsServerException(string message)\n            : base(message)\n        { }\n\n        public DnsServerException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Dnssec/DnssecEcdsaPrivateKey.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Security.Cryptography;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns.Dnssec;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Dnssec\n{\n    class DnssecEcdsaPrivateKey : DnssecPrivateKey\n    {\n        #region variables\n\n        ECParameters _ecdsaPrivateKey;\n\n        #endregion\n\n        #region constructor\n\n        public DnssecEcdsaPrivateKey(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, ECParameters ecdsaPrivateKey)\n            : base(algorithm, keyType)\n        {\n            _ecdsaPrivateKey = ecdsaPrivateKey;\n\n            InitDnsKey();\n        }\n\n        public DnssecEcdsaPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version)\n            : base(algorithm, bR, version)\n        {\n            InitDnsKey();\n        }\n\n        #endregion\n\n        #region private\n\n        private void InitDnsKey()\n        {\n            ECParameters ecdsaPublicKey = new ECParameters\n            {\n                Curve = _ecdsaPrivateKey.Curve,\n                Q = _ecdsaPrivateKey.Q\n            };\n\n            InitDnsKey(new DnssecEcdsaPublicKey(ecdsaPublicKey));\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override byte[] SignHash(byte[] hash)\n        {\n            using (ECDsa ecdsa = ECDsa.Create(_ecdsaPrivateKey))\n            {\n                return ecdsa.SignHash(hash, DSASignatureFormat.IeeeP1363FixedFieldConcatenation);\n            }\n        }\n\n        protected override void ReadPrivateKeyFrom(BinaryReader bR)\n        {\n            switch (Algorithm)\n            {\n                case DnssecAlgorithm.ECDSAP256SHA256:\n                    _ecdsaPrivateKey.Curve = ECCurve.NamedCurves.nistP256;\n                    break;\n\n                case DnssecAlgorithm.ECDSAP384SHA384:\n                    _ecdsaPrivateKey.Curve = ECCurve.NamedCurves.nistP384;\n                    break;\n\n                default:\n                    throw new InvalidDataException();\n            }\n\n            _ecdsaPrivateKey.D = bR.ReadBuffer();\n            _ecdsaPrivateKey.Q.X = bR.ReadBuffer();\n            _ecdsaPrivateKey.Q.Y = bR.ReadBuffer();\n        }\n\n        protected override void WritePrivateKeyTo(BinaryWriter bW)\n        {\n            bW.WriteBuffer(_ecdsaPrivateKey.D);\n            bW.WriteBuffer(_ecdsaPrivateKey.Q.X);\n            bW.WriteBuffer(_ecdsaPrivateKey.Q.Y);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Dnssec/DnssecEddsaPrivateKey.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing Org.BouncyCastle.Crypto;\nusing Org.BouncyCastle.Crypto.Parameters;\nusing Org.BouncyCastle.Crypto.Signers;\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns.Dnssec;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Dnssec\n{\n    class DnssecEddsaPrivateKey : DnssecPrivateKey\n    {\n        #region variables\n\n        Ed25519PrivateKeyParameters _ed25519PrivateKey;\n        Ed448PrivateKeyParameters _ed448PrivateKey;\n\n        #endregion\n\n        #region constructors\n\n        public DnssecEddsaPrivateKey(DnssecPrivateKeyType keyType, Ed25519PrivateKeyParameters ed25519PrivateKey)\n            : base(DnssecAlgorithm.ED25519, keyType)\n        {\n            _ed25519PrivateKey = ed25519PrivateKey;\n\n            InitDnsKey();\n        }\n\n        public DnssecEddsaPrivateKey(DnssecPrivateKeyType keyType, Ed448PrivateKeyParameters ed448PrivateKey)\n            : base(DnssecAlgorithm.ED448, keyType)\n        {\n            _ed448PrivateKey = ed448PrivateKey;\n\n            InitDnsKey();\n        }\n\n        public DnssecEddsaPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version)\n            : base(algorithm, bR, version)\n        {\n            InitDnsKey();\n        }\n\n        #endregion\n\n        #region private\n\n        private void InitDnsKey()\n        {\n            switch (Algorithm)\n            {\n                case DnssecAlgorithm.ED25519:\n                    InitDnsKey(new DnssecEddsaPublicKey(_ed25519PrivateKey.GeneratePublicKey()));\n                    break;\n\n                case DnssecAlgorithm.ED448:\n                    InitDnsKey(new DnssecEddsaPublicKey(_ed448PrivateKey.GeneratePublicKey()));\n                    break;\n            }\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override byte[] SignHash(byte[] hash)\n        {\n            ISigner signer;\n\n            switch (Algorithm)\n            {\n                case DnssecAlgorithm.ED25519:\n                    signer = new Ed25519Signer();\n                    signer.Init(true, _ed25519PrivateKey);\n                    break;\n\n                case DnssecAlgorithm.ED448:\n                    signer = new Ed448Signer([]);\n                    signer.Init(true, _ed448PrivateKey);\n                    break;\n\n                default:\n                    throw new InvalidOperationException();\n            }\n\n            signer.BlockUpdate(hash);\n\n            return signer.GenerateSignature();\n        }\n\n        protected override void ReadPrivateKeyFrom(BinaryReader bR)\n        {\n            switch (Algorithm)\n            {\n                case DnssecAlgorithm.ED25519:\n                    _ed25519PrivateKey = new Ed25519PrivateKeyParameters(bR.ReadBuffer());\n                    break;\n\n                case DnssecAlgorithm.ED448:\n                    _ed448PrivateKey = new Ed448PrivateKeyParameters(bR.ReadBuffer());\n                    break;\n\n                default:\n                    throw new InvalidDataException();\n            }\n        }\n\n        protected override void WritePrivateKeyTo(BinaryWriter bW)\n        {\n            switch (Algorithm)\n            {\n                case DnssecAlgorithm.ED25519:\n                    bW.WriteBuffer(_ed25519PrivateKey.GetEncoded());\n                    break;\n\n                case DnssecAlgorithm.ED448:\n                    bW.WriteBuffer(_ed448PrivateKey.GetEncoded());\n                    break;\n\n                default:\n                    throw new InvalidDataException();\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Zones;\nusing Org.BouncyCastle.Crypto.Parameters;\nusing Org.BouncyCastle.OpenSsl;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Security.Cryptography;\nusing System.Text;\nusing TechnitiumLibrary.Net.Dns.Dnssec;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Dnssec\n{\n    //DNSSEC Key Rollover Timing Considerations\n    //https://datatracker.ietf.org/doc/html/rfc7583\n\n    public enum DnssecPrivateKeyType : byte\n    {\n        Unknown = 0,\n        KeySigningKey = 1,\n        ZoneSigningKey = 2\n    }\n\n    public enum DnssecPrivateKeyState : byte\n    {\n        Unknown = 0,\n\n        /// <summary>\n        /// Although keys may be created immediately prior to first\n        /// use, some implementations may find it convenient to\n        /// create a pool of keys in one operation and draw from it\n        /// as required.  (Note: such a pre-generated pool must be\n        /// secured against surreptitious use.)  In the timelines\n        /// below, before the first event, the keys are considered to\n        /// be created but not yet used: they are said to be in the\n        /// \"Generated\" state.\n        /// </summary>\n        Generated = 1,\n\n        /// <summary>\n        /// A key enters the published state when either it or its associated data \n        /// first appears in the appropriate zone.\n        /// </summary>\n        Published = 2,\n\n        /// <summary>\n        /// The DNSKEY or its associated data have been published for long enough \n        /// to guarantee that copies of the key(s) it is replacing (or associated \n        /// data related to that key) have expired from caches.\n        /// </summary>\n        Ready = 3,\n\n        /// <summary>\n        /// The data is starting to be used for validation.  In the\n        /// case of a ZSK, it means that the key is now being used to\n        /// sign RRsets and that both it and the created RRSIGs\n        /// appear in the zone.  In the case of a KSK, it means that\n        /// it is possible to use it to validate a DNSKEY RRset as\n        /// both the DNSKEY and DS records are present in their\n        /// respective zones.  Note that when this state is entered,\n        /// it may not be possible for validating resolvers to use\n        /// the data for validation in all cases: the zone signing\n        /// may not have finished or the data might not have reached\n        /// the resolver because of propagation delays and/or caching\n        /// issues.  If this is the case, the resolver will have to\n        /// rely on the predecessor data instead.\n        /// </summary>\n        Active = 4,\n\n        /// <summary>\n        /// The data has ceased to be used for validation.  In the\n        /// case of a ZSK, it means that the key is no longer used to\n        /// sign RRsets.  In the case of a KSK, it means that the\n        /// successor DNSKEY and DS records are in place.  In both\n        /// cases, the key (and its associated data) can be removed\n        /// as soon as it is safe to do so, i.e., when all validating\n        /// resolvers are able to use the new key and associated data\n        /// to validate the zone.However, until this happens, the\n        /// current key and associated data must remain in their\n        /// respective zones.\n        /// </summary>\n        Retired = 5,\n\n        /// <summary>\n        /// The key and its associated data are present in their\n        /// respective zones, but there is no longer information\n        /// anywhere that requires their presence for use in\n        /// validation.  Hence, they can be removed at any time.\n        /// </summary>\n        Dead = 6,\n\n        /// <summary>\n        /// Both the DNSKEY and its associated data have been removed\n        /// from their respective zones.\n        /// </summary>\n        Removed = 7,\n\n        /// <summary>\n        /// The DNSKEY is published for a period with the \"revoke\"\n        /// bit set as a way of notifying validating resolvers that\n        /// have configured it as a trust anchor, as used in\n        /// [RFC5011], that it is about to be removed from the zone.\n        /// This state is used when [RFC5011] considerations are in\n        /// effect (see Section 3.3.4).\n        /// </summary>\n        Revoked = 8\n    }\n\n    public abstract class DnssecPrivateKey\n    {\n        #region variables\n\n        readonly DnssecAlgorithm _algorithm;\n        readonly DnssecPrivateKeyType _keyType;\n\n        DnssecPrivateKeyState _state;\n        DateTime _stateChangedOn;\n        DateTime _stateTransitionBy;\n        bool _isRetiring;\n        ushort _rolloverDays;\n\n        DnsDNSKEYRecordData _dnsKey;\n\n        #endregion\n\n        #region constructor\n\n        protected DnssecPrivateKey(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType)\n        {\n            _algorithm = algorithm;\n            _keyType = keyType;\n\n            _state = DnssecPrivateKeyState.Generated;\n            _stateChangedOn = DateTime.UtcNow;\n        }\n\n        protected DnssecPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version)\n        {\n            _algorithm = algorithm;\n            _keyType = (DnssecPrivateKeyType)bR.ReadByte();\n\n            _state = (DnssecPrivateKeyState)bR.ReadByte();\n            _stateChangedOn = DateTime.UnixEpoch.AddSeconds(bR.ReadInt64());\n\n            if (version >= 2)\n                _stateTransitionBy = DateTime.UnixEpoch.AddSeconds(bR.ReadInt64());\n\n            _isRetiring = bR.ReadBoolean();\n            _rolloverDays = bR.ReadUInt16();\n\n            ReadPrivateKeyFrom(bR);\n        }\n\n        #endregion\n\n        #region static\n\n        public static DnssecPrivateKey Create(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, int keySize = -1)\n        {\n            switch (algorithm)\n            {\n                case DnssecAlgorithm.RSAMD5:\n                case DnssecAlgorithm.RSASHA1:\n                case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:\n                case DnssecAlgorithm.RSASHA256:\n                case DnssecAlgorithm.RSASHA512:\n                    if ((keySize < 1024) || (keySize > 4096))\n                        throw new ArgumentOutOfRangeException(nameof(keySize), $\"Valid RSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? \"KSK\" : \"ZSK\")}) private key size range is between 1024-4096 bits.\");\n\n                    using (RSA rsa = RSA.Create(keySize))\n                    {\n                        return new DnssecRsaPrivateKey(algorithm, keyType, keySize, rsa.ExportParameters(true));\n                    }\n\n                case DnssecAlgorithm.ECDSAP256SHA256:\n                    using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256))\n                    {\n                        return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true));\n                    }\n\n                case DnssecAlgorithm.ECDSAP384SHA384:\n                    using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384))\n                    {\n                        return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true));\n                    }\n\n                case DnssecAlgorithm.ED25519:\n                    return new DnssecEddsaPrivateKey(keyType, new Ed25519PrivateKeyParameters(RandomNumberGenerator.GetBytes(32)));\n\n                case DnssecAlgorithm.ED448:\n                    return new DnssecEddsaPrivateKey(keyType, new Ed448PrivateKeyParameters(RandomNumberGenerator.GetBytes(57)));\n\n                default:\n                    throw new NotSupportedException(\"DNSSEC algorithm is not supported: \" + algorithm.ToString());\n            }\n        }\n\n        public static DnssecPrivateKey Create(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, string pemPrivateKey)\n        {\n            switch (algorithm)\n            {\n                case DnssecAlgorithm.RSAMD5:\n                case DnssecAlgorithm.RSASHA1:\n                case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:\n                case DnssecAlgorithm.RSASHA256:\n                case DnssecAlgorithm.RSASHA512:\n                    using (RSA rsa = RSA.Create())\n                    {\n                        rsa.ImportFromPem(pemPrivateKey);\n\n                        if ((rsa.KeySize < 1024) || (rsa.KeySize > 4096))\n                            throw new ArgumentOutOfRangeException(nameof(pemPrivateKey), $\"Valid RSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? \"KSK\" : \"ZSK\")}) private key size range is between 1024-4096 bits.\");\n\n                        return new DnssecRsaPrivateKey(algorithm, keyType, rsa.KeySize, rsa.ExportParameters(true));\n                    }\n\n                case DnssecAlgorithm.ECDSAP256SHA256:\n                    using (ECDsa ecdsa = ECDsa.Create())\n                    {\n                        ecdsa.ImportFromPem(pemPrivateKey);\n\n                        if (ecdsa.KeySize != 256)\n                            throw new ArgumentException($\"The ECDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? \"KSK\" : \"ZSK\")}) private key must have key size of 256 bits.\", nameof(pemPrivateKey));\n\n                        return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true));\n                    }\n\n                case DnssecAlgorithm.ECDSAP384SHA384:\n                    using (ECDsa ecdsa = ECDsa.Create())\n                    {\n                        ecdsa.ImportFromPem(pemPrivateKey);\n\n                        if (ecdsa.KeySize != 384)\n                            throw new ArgumentException($\"The ECDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? \"KSK\" : \"ZSK\")}) private key must have key size of 384 bits.\", nameof(pemPrivateKey));\n\n                        return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true));\n                    }\n\n                case DnssecAlgorithm.ED25519:\n                    using (PemReader pemReader = new PemReader(new StringReader(pemPrivateKey)))\n                    {\n                        if (pemReader.ReadObject() is not Ed25519PrivateKeyParameters privateKey)\n                            throw new ArgumentException($\"The EdDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? \"KSK\" : \"ZSK\")}) private key must be for Ed25519 curve.\", nameof(pemPrivateKey));\n\n                        return new DnssecEddsaPrivateKey(keyType, privateKey);\n                    }\n\n                case DnssecAlgorithm.ED448:\n                    using (PemReader pemReader = new PemReader(new StringReader(pemPrivateKey)))\n                    {\n                        if (pemReader.ReadObject() is not Ed448PrivateKeyParameters privateKey)\n                            throw new ArgumentException($\"The EdDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? \"KSK\" : \"ZSK\")}) private key must be for Ed448 curve.\", nameof(pemPrivateKey));\n\n                        return new DnssecEddsaPrivateKey(keyType, privateKey);\n                    }\n\n                default:\n                    throw new NotSupportedException(\"DNSSEC algorithm is not supported: \" + algorithm.ToString());\n            }\n        }\n\n        public static DnssecPrivateKey ReadFrom(BinaryReader bR)\n        {\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"DK\")\n                throw new InvalidDataException(\"DNSSEC private key format is invalid.\");\n\n            int version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                    DnssecAlgorithm algorithm = (DnssecAlgorithm)bR.ReadByte();\n                    switch (algorithm)\n                    {\n                        case DnssecAlgorithm.RSAMD5:\n                        case DnssecAlgorithm.RSASHA1:\n                        case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:\n                        case DnssecAlgorithm.RSASHA256:\n                        case DnssecAlgorithm.RSASHA512:\n                            return new DnssecRsaPrivateKey(algorithm, bR, version);\n\n                        case DnssecAlgorithm.ECDSAP256SHA256:\n                        case DnssecAlgorithm.ECDSAP384SHA384:\n                            return new DnssecEcdsaPrivateKey(algorithm, bR, version);\n\n                        case DnssecAlgorithm.ED25519:\n                        case DnssecAlgorithm.ED448:\n                            return new DnssecEddsaPrivateKey(algorithm, bR, version);\n\n                        default:\n                            throw new NotSupportedException(\"DNSSEC algorithm is not supported: \" + algorithm.ToString());\n                    }\n\n                default:\n                    throw new InvalidDataException(\"DNSSEC private key version not supported: \" + version);\n            }\n        }\n\n        #endregion\n\n        #region protected\n\n        protected void InitDnsKey(DnssecPublicKey publicKey)\n        {\n            DnsDnsKeyFlag flags = DnsDnsKeyFlag.ZoneKey;\n\n            if (KeyType == DnssecPrivateKeyType.KeySigningKey)\n                flags |= DnsDnsKeyFlag.SecureEntryPoint;\n\n            if (_state == DnssecPrivateKeyState.Revoked)\n                flags |= DnsDnsKeyFlag.Revoke;\n\n            _dnsKey = new DnsDNSKEYRecordData(flags, 3, _algorithm, publicKey);\n        }\n\n        protected abstract byte[] SignHash(byte[] hash);\n\n        protected abstract void ReadPrivateKeyFrom(BinaryReader bR);\n\n        protected abstract void WritePrivateKeyTo(BinaryWriter bW);\n\n        #endregion\n\n        #region internal\n\n        internal DnsResourceRecord SignRRSet(string signersName, IReadOnlyList<DnsResourceRecord> records, uint signatureInceptionOffset, uint signatureValidityPeriod)\n        {\n            DnsResourceRecord firstRecord = records[0];\n            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);\n\n            if (!DnsRRSIGRecordData.TryGetRRSetHash(unsignedRRSigRecord, records, out byte[] hash, out EDnsExtendedDnsErrorCode extendedDnsErrorCode))\n                throw new DnsServerException(\"Failed to sign record set: \" + extendedDnsErrorCode.ToString());\n\n            byte[] signature = SignHash(hash);\n\n            DnsRRSIGRecordData signedRRSigRecord = new DnsRRSIGRecordData(unsignedRRSigRecord.TypeCovered, unsignedRRSigRecord.Algorithm, unsignedRRSigRecord.Labels, unsignedRRSigRecord.OriginalTtl, unsignedRRSigRecord.SignatureExpiration, unsignedRRSigRecord.SignatureInception, unsignedRRSigRecord.KeyTag, unsignedRRSigRecord.SignersName, signature);\n            return new DnsResourceRecord(firstRecord.Name, DnsResourceRecordType.RRSIG, firstRecord.Class, firstRecord.OriginalTtlValue, signedRRSigRecord);\n        }\n\n        internal void SetState(DnssecPrivateKeyState state, uint stateTransitionInTtl = 0)\n        {\n            if (_state >= state)\n                return; //ignore; state cannot be updated to lower value\n\n            _state = state;\n            _stateChangedOn = DateTime.UtcNow;\n\n            if (stateTransitionInTtl > 0)\n                _stateTransitionBy = _stateChangedOn.AddSeconds(stateTransitionInTtl);\n            else\n                _stateTransitionBy = default;\n\n            if (_state == DnssecPrivateKeyState.Revoked)\n                InitDnsKey(_dnsKey.PublicKey);\n        }\n\n        internal void SetToRetire()\n        {\n            _isRetiring = true;\n        }\n\n        internal bool IsRolloverNeeded()\n        {\n            return (_rolloverDays > 0) && (DateTime.UtcNow > _stateChangedOn.AddDays(_rolloverDays));\n        }\n\n        internal void WriteTo(BinaryWriter bW)\n        {\n            bW.Write(Encoding.ASCII.GetBytes(\"DK\")); //format\n            bW.Write((byte)2); //version\n\n            bW.Write((byte)_algorithm);\n            bW.Write((byte)_keyType);\n\n            bW.Write((byte)_state);\n            bW.Write(Convert.ToInt64((_stateChangedOn - DateTime.UnixEpoch).TotalSeconds));\n            bW.Write(Convert.ToInt64((_stateTransitionBy - DateTime.UnixEpoch).TotalSeconds));\n            bW.Write(_isRetiring);\n            bW.Write(_rolloverDays);\n\n            WritePrivateKeyTo(bW);\n        }\n\n        #endregion\n\n        #region properties\n\n        public DnssecAlgorithm Algorithm\n        { get { return _algorithm; } }\n\n        public DnssecPrivateKeyType KeyType\n        { get { return _keyType; } }\n\n        public DnssecPrivateKeyState State\n        { get { return _state; } }\n\n        public DateTime StateChangedOn\n        { get { return _stateChangedOn; } }\n\n        public DateTime StateTransitionBy\n        { get { return _stateTransitionBy; } }\n\n        public DateTime StateTransitionByWithDelays\n        { get { return _stateTransitionBy.AddMilliseconds(PrimaryZone.DNSSEC_TIMER_PERIODIC_INTERVAL); } }\n\n        public bool IsRetiring\n        { get { return _isRetiring; } }\n\n        public ushort RolloverDays\n        {\n            get { return _rolloverDays; }\n            set\n            {\n                if (_keyType == DnssecPrivateKeyType.ZoneSigningKey)\n                {\n                    if (value > 365)\n                        throw new ArgumentOutOfRangeException(nameof(RolloverDays), \"Zone Signing Key (ZSK) automatic rollover days valid range is 0-365.\");\n\n                    switch (_state)\n                    {\n                        case DnssecPrivateKeyState.Generated:\n                        case DnssecPrivateKeyState.Published:\n                        case DnssecPrivateKeyState.Ready:\n                        case DnssecPrivateKeyState.Active:\n                            if (_isRetiring)\n                                throw new InvalidOperationException(\"Zone Signing Key (ZSK) automatic rollover cannot be set since it is set to retire.\");\n\n                            break;\n\n                        default:\n                            throw new InvalidOperationException(\"Zone Signing Key (ZSK) automatic rollover cannot be set due to invalid key state.\");\n                    }\n                }\n                else\n                {\n                    if (value != 0)\n                        throw new NotSupportedException(\"Automatic rollover is not supported for Key Signing Keys (KSK).\");\n                }\n\n                _rolloverDays = value;\n            }\n        }\n\n        public DnsDNSKEYRecordData DnsKey\n        { get { return _dnsKey; } }\n\n        public ushort KeyTag\n        { get { return _dnsKey.ComputedKeyTag; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Dnssec/DnssecRsaPrivateKey.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\nusing System.Security.Cryptography;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns.Dnssec;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Dnssec\n{\n    class DnssecRsaPrivateKey : DnssecPrivateKey\n    {\n        #region variables\n\n        int _keySize;\n        RSAParameters _rsaPrivateKey;\n        readonly HashAlgorithmName _hashAlgorithm;\n\n        #endregion\n\n        #region constructor\n\n        public DnssecRsaPrivateKey(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, int keySize, RSAParameters rsaPrivateKey)\n            : base(algorithm, keyType)\n        {\n            _keySize = keySize;\n            _rsaPrivateKey = rsaPrivateKey;\n\n            _hashAlgorithm = DnsRRSIGRecordData.GetHashAlgorithmName(algorithm);\n            InitDnsKey();\n        }\n\n        public DnssecRsaPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version)\n            : base(algorithm, bR, version)\n        {\n            _hashAlgorithm = DnsRRSIGRecordData.GetHashAlgorithmName(algorithm);\n            InitDnsKey();\n        }\n\n        #endregion\n\n        #region private\n\n        private void InitDnsKey()\n        {\n            RSAParameters rsaPublicKey = new RSAParameters\n            {\n                Exponent = _rsaPrivateKey.Exponent,\n                Modulus = _rsaPrivateKey.Modulus\n            };\n\n            InitDnsKey(new DnssecRsaPublicKey(rsaPublicKey));\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override byte[] SignHash(byte[] hash)\n        {\n            using (RSA rsa = RSA.Create(_rsaPrivateKey))\n            {\n                return rsa.SignHash(hash, _hashAlgorithm, RSASignaturePadding.Pkcs1);\n            }\n        }\n\n        protected override void ReadPrivateKeyFrom(BinaryReader bR)\n        {\n            _keySize = bR.ReadInt32();\n\n            _rsaPrivateKey.D = bR.ReadBuffer();\n            _rsaPrivateKey.DP = bR.ReadBuffer();\n            _rsaPrivateKey.DQ = bR.ReadBuffer();\n            _rsaPrivateKey.Exponent = bR.ReadBuffer();\n            _rsaPrivateKey.InverseQ = bR.ReadBuffer();\n            _rsaPrivateKey.Modulus = bR.ReadBuffer();\n            _rsaPrivateKey.P = bR.ReadBuffer();\n            _rsaPrivateKey.Q = bR.ReadBuffer();\n        }\n\n        protected override void WritePrivateKeyTo(BinaryWriter bW)\n        {\n            bW.Write(_keySize);\n\n            bW.WriteBuffer(_rsaPrivateKey.D);\n            bW.WriteBuffer(_rsaPrivateKey.DP);\n            bW.WriteBuffer(_rsaPrivateKey.DQ);\n            bW.WriteBuffer(_rsaPrivateKey.Exponent);\n            bW.WriteBuffer(_rsaPrivateKey.InverseQ);\n            bW.WriteBuffer(_rsaPrivateKey.Modulus);\n            bW.WriteBuffer(_rsaPrivateKey.P);\n            bW.WriteBuffer(_rsaPrivateKey.Q);\n        }\n\n        #endregion\n\n        #region protected\n\n        public int KeySize\n        { get { return _keySize; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResolverDnsCache.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns\n{\n    class ResolverDnsCache : IDnsCache\n    {\n        #region variables\n\n        readonly DnsServer _dnsServer;\n        readonly bool _skipDnsAppAuthoritativeRequestHandlers;\n        readonly bool _skipConditionalForwardingResolution;\n\n        #endregion\n\n        #region constructor\n\n        public ResolverDnsCache(DnsServer dnsServer, bool skipDnsAppAuthoritativeRequestHandlers, bool skipConditionalForwardingResolution = false)\n        {\n            _dnsServer = dnsServer;\n            _skipDnsAppAuthoritativeRequestHandlers = skipDnsAppAuthoritativeRequestHandlers;\n            _skipConditionalForwardingResolution = skipConditionalForwardingResolution;\n        }\n\n        #endregion\n\n        #region private\n\n        private async Task<DnsDatagram> AuthoritativeQueryClosestDelegation(DnsDatagram request)\n        {\n            DnsDatagram authResponse = _dnsServer.AuthZoneManager.QueryClosestDelegation(request);\n\n            DnsDatagram appResponse = await DnsApplicationQueryClosestDelegationAsync(request);\n\n            if ((authResponse is not null) && (authResponse.Authority.Count > 0))\n            {\n                if ((appResponse is not null) && (appResponse.Authority.Count > 0))\n                {\n                    DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord();\n                    DnsResourceRecord appResponseFirstAuthority = appResponse.FindFirstAuthorityRecord();\n\n                    if (appResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length)\n                        return appResponse;\n                }\n\n                return authResponse;\n            }\n            else\n            {\n                return appResponse;\n            }\n        }\n\n        private async Task<DnsDatagram> DnsApplicationQueryClosestDelegationAsync(DnsDatagram request)\n        {\n            if (_skipDnsAppAuthoritativeRequestHandlers || (_dnsServer.DnsApplicationManager.DnsAuthoritativeRequestHandlers.Count < 1) || (request.Question.Count != 1))\n                return null;\n\n            IPEndPoint localEP = new IPEndPoint(IPAddress.Any, 0);\n            DnsQuestionRecord question = request.Question[0];\n            string currentDomain = question.Name;\n\n            while (true)\n            {\n                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) });\n\n                foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsServer.DnsApplicationManager.DnsAuthoritativeRequestHandlers)\n                {\n                    try\n                    {\n                        DnsDatagram nsResponse = await requestHandler.ProcessRequestAsync(nsRequest, localEP, DnsTransportProtocol.Tcp, false);\n                        if (nsResponse is not null)\n                        {\n                            if ((nsResponse.Answer.Count > 0) && (nsResponse.Answer[0].Type == DnsResourceRecordType.NS))\n                                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);\n                            else if ((nsResponse.Authority.Count > 0) && (nsResponse.FindFirstAuthorityType() == DnsResourceRecordType.NS))\n                                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);\n                        }\n                    }\n                    catch (DnsClientException ex)\n                    {\n                        _dnsServer.ResolverLogManager?.Write(ex);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                }\n\n                //get parent domain\n                int i = currentDomain.IndexOf('.');\n                if (i < 0)\n                    break;\n\n                currentDomain = currentDomain.Substring(i + 1);\n            }\n\n            return null;\n        }\n\n        private Task<DnsDatagram> DoConditionalForwardingResolutionAsync(DnsDatagram request, IReadOnlyList<DnsResourceRecord> conditionalForwarders)\n        {\n            DnsQuestionRecord question = request.Question[0];\n            NetworkAddress eDnsClientSubnet = null;\n            bool advancedForwardingClientSubnet = false; //this feature is used by Advanced Forwarding app to cache response per network group\n\n            EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n            if (requestECS is not null)\n            {\n                //use ECS from client request\n                switch (requestECS.Family)\n                {\n                    case EDnsClientSubnetAddressFamily.IPv4:\n                        eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength);\n                        break;\n\n                    case EDnsClientSubnetAddressFamily.IPv6:\n                        eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength);\n                        break;\n                }\n\n                advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet;\n            }\n\n            ResolverDnsCache dnsCache = new ResolverDnsCache(_dnsServer, _skipDnsAppAuthoritativeRequestHandlers, true);\n\n            return _dnsServer.PriorityConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, _skipDnsAppAuthoritativeRequestHandlers, conditionalForwarders);\n        }\n\n        #endregion\n\n        #region protected\n\n        protected async Task<DnsDatagram> QueryClosestDelegationAsync(DnsDatagram request)\n        {\n            DnsDatagram authResponse = await AuthoritativeQueryClosestDelegation(request);\n\n            DnsDatagram cacheResponse = await _dnsServer.CacheZoneManager.QueryClosestDelegationAsync(request);\n\n            if ((authResponse is not null) && (authResponse.Authority.Count > 0))\n            {\n                if ((cacheResponse is not null) && (cacheResponse.Authority.Count > 0))\n                {\n                    DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord();\n                    DnsResourceRecord cacheResponseFirstAuthority = cacheResponse.FindFirstAuthorityRecord();\n\n                    if (cacheResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length)\n                        return cacheResponse;\n                }\n\n                return authResponse;\n            }\n            else\n            {\n                return cacheResponse;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public virtual async Task<DnsDatagram> QueryAsync(DnsDatagram request, bool serveStale, bool findClosestNameServers = false, bool resetExpiry = false)\n        {\n            DnsDatagram authResponse = await _dnsServer.AuthoritativeQueryAsync(request, DnsTransportProtocol.Tcp, true, _skipDnsAppAuthoritativeRequestHandlers);\n            if (authResponse is not null)\n            {\n                if ((authResponse.RCODE != DnsResponseCode.NoError) || (authResponse.Answer.Count > 0) || (authResponse.Authority.Count == 0) || authResponse.IsFirstAuthoritySOA())\n                    return authResponse;\n            }\n\n            DnsDatagram cacheResponse = await _dnsServer.CacheZoneManager.QueryAsync(request, serveStale, findClosestNameServers, resetExpiry);\n            if (cacheResponse is not null)\n            {\n                if ((cacheResponse.RCODE != DnsResponseCode.NoError) || (cacheResponse.Answer.Count > 0) || (cacheResponse.Authority.Count == 0) || cacheResponse.IsFirstAuthoritySOA())\n                    return cacheResponse;\n            }\n\n            if ((authResponse is not null) && (authResponse.Authority.Count > 0))\n            {\n                if ((cacheResponse is not null) && (cacheResponse.Authority.Count > 0))\n                {\n                    DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord();\n                    DnsResourceRecord cacheResponseFirstAuthority = cacheResponse.FindFirstAuthorityRecord();\n\n                    if (cacheResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length)\n                        return cacheResponse;\n                }\n\n                if (!_skipConditionalForwardingResolution)\n                {\n                    DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord();\n                    if (authResponseFirstAuthority.Type == DnsResourceRecordType.FWD)\n                        return await DoConditionalForwardingResolutionAsync(request, authResponse.Authority);\n                }\n\n                return authResponse;\n            }\n            else\n            {\n                return cacheResponse;\n            }\n        }\n\n        public void CacheResponse(DnsDatagram response, bool isDnssecBadCache = false, string zoneCut = null)\n        {\n            _dnsServer.CacheZoneManager.CacheResponse(response, isDnssecBadCache, zoneCut);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResolverPrefetchDnsCache.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.Dns\n{\n    class ResolverPrefetchDnsCache : ResolverDnsCache\n    {\n        #region variables\n\n        readonly DnsQuestionRecord _prefetchQuestion;\n\n        #endregion\n\n        #region constructor\n\n        public ResolverPrefetchDnsCache(DnsServer dnsServer, bool skipDnsAppAuthoritativeRequestHandlers, DnsQuestionRecord prefetchQuestion)\n            : base(dnsServer, skipDnsAppAuthoritativeRequestHandlers)\n        {\n            _prefetchQuestion = prefetchQuestion;\n        }\n\n        #endregion\n\n        #region public\n\n        public override Task<DnsDatagram> QueryAsync(DnsDatagram request, bool serveStale = false, bool findClosestNameServers = false, bool resetExpiry = false)\n        {\n            if (_prefetchQuestion.Equals(request.Question[0]))\n            {\n                //request is for prefetch question\n\n                if (!findClosestNameServers)\n                    return Task.FromResult<DnsDatagram>(null); //dont give answer from cache for prefetch question\n\n                //return closest name servers so that the recursive resolver queries them to refreshes cache instead of returning response from cache\n                return QueryClosestDelegationAsync(request);\n            }\n\n            return base.QueryAsync(request, serveStale, findClosestNameServers, resetExpiry);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/AuthRecordInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing System.Net;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    abstract class AuthRecordInfo\n    {\n        #region constructor\n\n        protected AuthRecordInfo()\n        { }\n\n        protected AuthRecordInfo(BinaryReader bR)\n        {\n            byte version = bR.ReadByte();\n            if (version >= 9)\n                ReadRecordInfoFrom(bR);\n            else\n                ReadOldFormatFrom(bR, version, this is SOARecordInfo);\n        }\n\n        #endregion\n\n        #region static\n\n        public static GenericRecordInfo ReadGenericRecordInfoFrom(BinaryReader bR, DnsResourceRecordType type)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.NS:\n                    return new NSRecordInfo(bR);\n\n                case DnsResourceRecordType.SOA:\n                    return new SOARecordInfo(bR);\n\n                case DnsResourceRecordType.SVCB:\n                case DnsResourceRecordType.HTTPS:\n                    return new SVCBRecordInfo(bR);\n\n                default:\n                    return new GenericRecordInfo(bR);\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private void ReadOldFormatFrom(BinaryReader bR, byte version, bool isSoa)\n        {\n            switch (version)\n            {\n                case 1:\n                    {\n                        bool disabled = bR.ReadBoolean();\n\n                        if (this is GenericRecordInfo info)\n                            info.Disabled = disabled;\n                    }\n                    break;\n\n                case 2:\n                case 3:\n                case 4:\n                case 5:\n                case 6:\n                case 7:\n                case 8:\n                    {\n                        {\n                            bool disabled = bR.ReadBoolean();\n\n                            if (this is GenericRecordInfo info)\n                                info.Disabled = disabled;\n                        }\n\n                        if ((version < 5) && isSoa)\n                        {\n                            //read old glue records as NameServerAddress in case of SOA record\n                            int count = bR.ReadByte();\n                            if (count > 0)\n                            {\n                                NameServerAddress[] primaryNameServers = new NameServerAddress[count];\n\n                                for (int i = 0; i < primaryNameServers.Length; i++)\n                                {\n                                    DnsResourceRecord glueRecord = new DnsResourceRecord(bR.BaseStream);\n\n                                    IPAddress address;\n\n                                    switch (glueRecord.Type)\n                                    {\n                                        case DnsResourceRecordType.A:\n                                            address = (glueRecord.RDATA as DnsARecordData).Address;\n                                            break;\n\n                                        case DnsResourceRecordType.AAAA:\n                                            address = (glueRecord.RDATA as DnsAAAARecordData).Address;\n                                            break;\n\n                                        default:\n                                            continue;\n                                    }\n\n                                    primaryNameServers[i] = new NameServerAddress(address);\n                                }\n\n                                (this as SOARecordInfo).PrimaryNameServers = primaryNameServers;\n                            }\n                        }\n                        else\n                        {\n                            int count = bR.ReadByte();\n                            if (count > 0)\n                            {\n                                DnsResourceRecord[] glueRecords = new DnsResourceRecord[count];\n\n                                for (int i = 0; i < glueRecords.Length; i++)\n                                    glueRecords[i] = new DnsResourceRecord(bR.BaseStream);\n\n                                if (this is NSRecordInfo info)\n                                    info.GlueRecords = glueRecords;\n                            }\n                        }\n\n                        if (version >= 3)\n                        {\n                            string comments = bR.ReadShortString();\n\n                            if (this is GenericRecordInfo info)\n                                info.Comments = comments;\n                        }\n\n                        if (version >= 4)\n                        {\n                            DateTime deletedOn = bR.ReadDateTime();\n\n                            if (this is HistoryRecordInfo info)\n                                info.DeletedOn = deletedOn;\n                        }\n\n                        if (version >= 5)\n                        {\n                            int count = bR.ReadByte();\n                            if (count > 0)\n                            {\n                                NameServerAddress[] primaryNameServers = new NameServerAddress[count];\n\n                                for (int i = 0; i < primaryNameServers.Length; i++)\n                                    primaryNameServers[i] = new NameServerAddress(bR);\n\n                                if (this is SOARecordInfo info)\n                                    info.PrimaryNameServers = primaryNameServers;\n                            }\n                        }\n\n                        if (version >= 7)\n                        {\n                            DnsTransportProtocol zoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte();\n                            string tsigKeyName = bR.ReadShortString();\n\n                            if (this is SOARecordInfo info)\n                            {\n                                if (zoneTransferProtocol != DnsTransportProtocol.Udp)\n                                    info.ZoneTransferProtocol = zoneTransferProtocol;\n\n                                if (tsigKeyName.Length > 0)\n                                    info.TsigKeyName = tsigKeyName;\n                            }\n                        }\n                        else if (version >= 6)\n                        {\n                            DnsTransportProtocol zoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte();\n\n                            string tsigKeyName = bR.ReadShortString();\n                            _ = bR.ReadShortString(); //_tsigSharedSecret (obsolete)\n                            _ = bR.ReadShortString(); //_tsigAlgorithm (obsolete)\n\n                            if (this is SOARecordInfo info)\n                            {\n                                if (zoneTransferProtocol != DnsTransportProtocol.Udp)\n                                    info.ZoneTransferProtocol = zoneTransferProtocol;\n\n                                if (tsigKeyName.Length > 0)\n                                    info.TsigKeyName = tsigKeyName;\n                            }\n                        }\n\n                        if (version >= 8)\n                        {\n                            bool useSoaSerialDateScheme = bR.ReadBoolean();\n\n                            if (this is SOARecordInfo info)\n                                info.UseSoaSerialDateScheme = useSoaSerialDateScheme;\n                        }\n                    }\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"AuthRecordInfo format version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region protected\n\n        protected abstract void ReadRecordInfoFrom(BinaryReader bR);\n\n        protected abstract void WriteRecordInfoTo(BinaryWriter bW);\n\n        #endregion\n\n        #region public\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)9); //version\n\n            WriteRecordInfoTo(bW);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/CacheRecordInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class CacheRecordInfo\n    {\n        #region variables\n\n        public static readonly CacheRecordInfo Default = new CacheRecordInfo();\n\n        IReadOnlyList<DnsResourceRecord> _glueRecords;\n        IReadOnlyList<DnsResourceRecord> _rrsigRecords;\n        IReadOnlyList<DnsResourceRecord> _nsecRecords;\n        NetworkAddress _eDnsClientSubnet;\n        DnsDatagramMetadata _responseMetadata;\n\n        DateTime _lastUsedOn; //not serialized\n\n        #endregion\n\n        #region constructor\n\n        public CacheRecordInfo()\n        { }\n\n        public CacheRecordInfo(BinaryReader bR)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                    _glueRecords = ReadRecordsFrom(bR, true);\n                    _rrsigRecords = ReadRecordsFrom(bR, false);\n                    _nsecRecords = ReadRecordsFrom(bR, true);\n\n                    if (bR.ReadBoolean())\n                        _eDnsClientSubnet = NetworkAddress.ReadFrom(bR);\n\n                    if (version >= 2)\n                    {\n                        if (bR.ReadBoolean())\n                            _responseMetadata = new DnsDatagramMetadata(bR);\n                    }\n\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"CacheRecordInfo format version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private static DnsResourceRecord[] ReadRecordsFrom(BinaryReader bR, bool includeInnerRRSigRecords)\n        {\n            int count = bR.ReadByte();\n            if (count == 0)\n                return null;\n\n            DnsResourceRecord[] records = new DnsResourceRecord[count];\n\n            for (int i = 0; i < count; i++)\n            {\n                records[i] = DnsResourceRecord.ReadCacheRecordFrom(bR, delegate (DnsResourceRecord record)\n                {\n                    if (includeInnerRRSigRecords)\n                    {\n                        IReadOnlyList<DnsResourceRecord> rrsigRecords = ReadRecordsFrom(bR, false);\n                        if (rrsigRecords is not null)\n                            record.GetCacheRecordInfo()._rrsigRecords = rrsigRecords;\n                    }\n                });\n            }\n\n            return records;\n        }\n\n        private static void WriteRecordsTo(IReadOnlyList<DnsResourceRecord> records, BinaryWriter bW, bool includeInnerRRSigRecords)\n        {\n            if (records is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(records.Count));\n\n                foreach (DnsResourceRecord record in records)\n                {\n                    record.WriteCacheRecordTo(bW, delegate ()\n                    {\n                        if (includeInnerRRSigRecords)\n                        {\n                            if (record.Tag is CacheRecordInfo cacheRecordInfo)\n                                WriteRecordsTo(cacheRecordInfo._rrsigRecords, bW, false);\n                            else\n                                bW.Write((byte)0);\n                        }\n                    });\n                }\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)2); //version\n\n            WriteRecordsTo(_glueRecords, bW, true);\n            WriteRecordsTo(_rrsigRecords, bW, false);\n            WriteRecordsTo(_nsecRecords, bW, true);\n\n            if (_eDnsClientSubnet is null)\n            {\n                bW.Write(false);\n            }\n            else\n            {\n                bW.Write(true);\n                _eDnsClientSubnet.WriteTo(bW);\n            }\n\n            if (_responseMetadata is null)\n            {\n                bW.Write(false);\n            }\n            else\n            {\n                bW.Write(true);\n                _responseMetadata.WriteTo(bW);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyList<DnsResourceRecord> GlueRecords\n        {\n            get { return _glueRecords; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _glueRecords = null;\n                else\n                    _glueRecords = value;\n            }\n        }\n\n        public IReadOnlyList<DnsResourceRecord> RRSIGRecords\n        {\n            get { return _rrsigRecords; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _rrsigRecords = null;\n                else\n                    _rrsigRecords = value;\n            }\n        }\n\n        public IReadOnlyList<DnsResourceRecord> NSECRecords\n        {\n            get { return _nsecRecords; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _nsecRecords = null;\n                else\n                    _nsecRecords = value;\n            }\n        }\n\n        public NetworkAddress EDnsClientSubnet\n        {\n            get { return _eDnsClientSubnet; }\n            set { _eDnsClientSubnet = value; }\n        }\n\n        public DnsDatagramMetadata ResponseMetadata\n        {\n            get { return _responseMetadata; }\n            set { _responseMetadata = value; }\n        }\n\n        public DateTime LastUsedOn\n        {\n            get { return _lastUsedOn; }\n            set { _lastUsedOn = value; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/DnsNSRecordDataExtended.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class DnsNSRecordDataExtended : DnsNSRecordData\n    {\n        #region constructors\n\n        public DnsNSRecordDataExtended(string nameServer, bool validateName = true)\n            : base(nameServer, validateName)\n        { }\n\n        #endregion\n\n        #region public\n\n        public void UpdateNameServer(string nameServer)\n        {\n            _nameServer = nameServer;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtensions.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Sockets;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    static class DnsResourceRecordExtensions\n    {\n        public static void SetGlueRecords(this DnsResourceRecord record, string glueAddresses)\n        {\n            if (record.RDATA is not DnsNSRecordData nsRecord)\n                throw new InvalidOperationException();\n\n            string domain = nsRecord.NameServer;\n\n            IPAddress[] glueAddressesList = glueAddresses.Split(IPAddress.Parse, ',');\n            DnsResourceRecord[] glueRecords = new DnsResourceRecord[glueAddressesList.Length];\n\n            for (int i = 0; i < glueRecords.Length; i++)\n            {\n                switch (glueAddressesList[i].AddressFamily)\n                {\n                    case AddressFamily.InterNetwork:\n                        glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.A, DnsClass.IN, record.TTL, new DnsARecordData(glueAddressesList[i]));\n                        break;\n\n                    case AddressFamily.InterNetworkV6:\n                        glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.AAAA, DnsClass.IN, record.TTL, new DnsAAAARecordData(glueAddressesList[i]));\n                        break;\n                }\n            }\n\n            record.GetAuthNSRecordInfo().GlueRecords = glueRecords;\n        }\n\n        public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyList<DnsResourceRecord> allGlueRecords)\n        {\n            if (record.RDATA is not DnsNSRecordData nsRecord)\n                throw new InvalidOperationException();\n\n            string domain = nsRecord.NameServer;\n\n            List<DnsResourceRecord> foundGlueRecords = new List<DnsResourceRecord>(2);\n\n            foreach (DnsResourceRecord glueRecord in allGlueRecords)\n            {\n                switch (glueRecord.Type)\n                {\n                    case DnsResourceRecordType.A:\n                    case DnsResourceRecordType.AAAA:\n                        if (glueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                            foundGlueRecords.Add(glueRecord);\n\n                        break;\n                }\n            }\n\n            record.GetAuthNSRecordInfo().GlueRecords = foundGlueRecords;\n        }\n\n        public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyCollection<DnsResourceRecord> deletedGlueRecords, IReadOnlyCollection<DnsResourceRecord> addedGlueRecords)\n        {\n            if (record.RDATA is not DnsNSRecordData nsRecord)\n                throw new InvalidOperationException();\n\n            bool updated = false;\n\n            List<DnsResourceRecord> updatedGlueRecords = new List<DnsResourceRecord>();\n            IReadOnlyList<DnsResourceRecord> existingGlueRecords = record.GetAuthNSRecordInfo().GlueRecords;\n            if (existingGlueRecords is not null)\n            {\n                foreach (DnsResourceRecord existingGlueRecord in existingGlueRecords)\n                {\n                    if (deletedGlueRecords.Contains(existingGlueRecord))\n                        updated = true; //skipped to delete existing glue record\n                    else\n                        updatedGlueRecords.Add(existingGlueRecord);\n                }\n            }\n\n            string domain = nsRecord.NameServer;\n\n            foreach (DnsResourceRecord addedGlueRecord in addedGlueRecords)\n            {\n                switch (addedGlueRecord.Type)\n                {\n                    case DnsResourceRecordType.A:\n                    case DnsResourceRecordType.AAAA:\n                        if (addedGlueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                        {\n                            updatedGlueRecords.Add(addedGlueRecord);\n                            updated = true;\n                        }\n                        break;\n                }\n            }\n\n            if (updated)\n                record.GetAuthNSRecordInfo().GlueRecords = updatedGlueRecords;\n        }\n\n        public static GenericRecordInfo GetAuthGenericRecordInfo(this DnsResourceRecord record)\n        {\n            if (record.Tag is null)\n            {\n                GenericRecordInfo rrInfo;\n\n                switch (record.Type)\n                {\n                    case DnsResourceRecordType.NS:\n                        rrInfo = new NSRecordInfo();\n                        break;\n\n                    case DnsResourceRecordType.SOA:\n                        rrInfo = new SOARecordInfo();\n                        break;\n\n                    case DnsResourceRecordType.SVCB:\n                    case DnsResourceRecordType.HTTPS:\n                        rrInfo = new SVCBRecordInfo();\n                        break;\n\n                    default:\n                        rrInfo = new GenericRecordInfo();\n                        break;\n                }\n\n                record.Tag = rrInfo;\n\n                return rrInfo;\n            }\n            else if (record.Tag is GenericRecordInfo rrInfo)\n            {\n                return rrInfo;\n            }\n            else\n            {\n                throw new InvalidOperationException();\n            }\n        }\n\n        public static NSRecordInfo GetAuthNSRecordInfo(this DnsResourceRecord record)\n        {\n            if (record.Tag is null)\n            {\n                NSRecordInfo info = new NSRecordInfo();\n                record.Tag = info;\n\n                return info;\n            }\n            else if (record.Tag is NSRecordInfo nsInfo)\n            {\n                return nsInfo;\n            }\n            else\n            {\n                throw new InvalidOperationException();\n            }\n        }\n\n        public static SOARecordInfo GetAuthSOARecordInfo(this DnsResourceRecord record)\n        {\n            if (record.Tag is null)\n            {\n                SOARecordInfo info = new SOARecordInfo();\n                record.Tag = info;\n\n                return info;\n            }\n            else if (record.Tag is SOARecordInfo soaInfo)\n            {\n                return soaInfo;\n            }\n            else\n            {\n                throw new InvalidOperationException();\n            }\n        }\n\n        public static SVCBRecordInfo GetAuthSVCBRecordInfo(this DnsResourceRecord record)\n        {\n            if (record.Tag is null)\n            {\n                SVCBRecordInfo info = new SVCBRecordInfo();\n                record.Tag = info;\n\n                return info;\n            }\n            else if (record.Tag is SVCBRecordInfo svcbInfo)\n            {\n                return svcbInfo;\n            }\n            else\n            {\n                throw new InvalidOperationException();\n            }\n        }\n\n        public static HistoryRecordInfo GetAuthHistoryRecordInfo(this DnsResourceRecord record)\n        {\n            if (record.Tag is null)\n            {\n                HistoryRecordInfo info = new HistoryRecordInfo();\n                record.Tag = info;\n\n                return info;\n            }\n            else if (record.Tag is HistoryRecordInfo info)\n            {\n                return info;\n            }\n            else\n            {\n                throw new InvalidOperationException();\n            }\n        }\n\n        public static CacheRecordInfo GetCacheRecordInfo(this DnsResourceRecord record)\n        {\n            if (record.Tag is not CacheRecordInfo rrInfo)\n            {\n                rrInfo = new CacheRecordInfo();\n                record.Tag = rrInfo;\n            }\n\n            return rrInfo;\n        }\n\n        public static void CopyRecordInfoFrom(this DnsResourceRecord record, DnsResourceRecord otherRecord)\n        {\n            record.Tag = otherRecord.Tag;\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/DnsSOARecordDataExtended.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class DnsSOARecordDataExtended : DnsSOARecordData\n    {\n        #region constructor\n\n        public DnsSOARecordDataExtended(string primaryNameServer, string responsiblePerson, uint serial, uint refresh, uint retry, uint expire, uint minimum)\n            : base(primaryNameServer, responsiblePerson, serial, refresh, retry, expire, minimum)\n        { }\n\n        #endregion\n\n        #region public\n\n        public void UpdatePrimaryNameServerAndMinimum(string primaryNameServer, uint minimum)\n        {\n            _primaryNameServer = primaryNameServer;\n            _minimum = minimum;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/GenericRecordInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class GenericRecordInfo : AuthRecordInfo\n    {\n        #region variables\n\n        bool _disabled;\n        string _comments;\n        DateTime _lastModified;\n        uint _expiryTtl;\n\n        DateTime _lastUsedOn; //not serialized\n\n        #endregion\n\n        #region constructor\n\n        public GenericRecordInfo()\n        { }\n\n        public GenericRecordInfo(BinaryReader bR)\n            : base(bR)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected sealed override void ReadRecordInfoFrom(BinaryReader bR)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    _disabled = bR.ReadBoolean();\n                    _comments = bR.ReadShortString();\n\n                    ReadExtendedRecordInfoFrom(bR);\n                    break;\n\n                case 2:\n                    _disabled = bR.ReadBoolean();\n                    _comments = bR.ReadShortString();\n\n                    _lastModified = bR.ReadDateTime();\n                    _expiryTtl = bR.ReadUInt32();\n\n                    ReadExtendedRecordInfoFrom(bR);\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"GenericRecordInfo format version not supported.\");\n            }\n        }\n\n        protected sealed override void WriteRecordInfoTo(BinaryWriter bW)\n        {\n            bW.Write((byte)2); //version\n\n            bW.Write(_disabled);\n\n            if (string.IsNullOrEmpty(_comments))\n                bW.Write((byte)0);\n            else\n                bW.WriteShortString(_comments);\n\n            bW.Write(_lastModified);\n            bW.Write(_expiryTtl);\n\n            WriteExtendedRecordInfoTo(bW);\n        }\n\n        protected virtual void ReadExtendedRecordInfoFrom(BinaryReader bR)\n        {\n            _ = bR.ReadByte(); //read byte to move ahead\n        }\n\n        protected virtual void WriteExtendedRecordInfoTo(BinaryWriter bW)\n        {\n            bW.Write((byte)0); //no extended info\n        }\n\n        #endregion\n\n        #region public\n\n        public uint GetPendingExpiryTtl()\n        {\n            uint elapsedSeconds = Convert.ToUInt32((DateTime.UtcNow - _lastModified).TotalSeconds);\n            if (elapsedSeconds < _expiryTtl)\n                return _expiryTtl - elapsedSeconds;\n\n            return 0u;\n        }\n\n        #endregion\n\n        #region properties\n\n        public virtual bool Disabled\n        {\n            get { return _disabled; }\n            set { _disabled = value; }\n        }\n\n        public string Comments\n        {\n            get { return _comments; }\n            set\n            {\n                if ((value is not null) && (value.Length > 255))\n                    throw new ArgumentOutOfRangeException(nameof(Comments), \"Resource record comment text cannot exceed 255 characters.\");\n\n                _comments = value;\n            }\n        }\n\n        public DateTime LastModified\n        {\n            get { return _lastModified; }\n            set { _lastModified = value; }\n        }\n\n        public virtual uint ExpiryTtl\n        {\n            get { return _expiryTtl; }\n            set { _expiryTtl = value; }\n        }\n\n        public DateTime LastUsedOn\n        {\n            get { return _lastUsedOn; }\n            set { _lastUsedOn = value; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/HistoryRecordInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.IO;\nusing TechnitiumLibrary.IO;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class HistoryRecordInfo : AuthRecordInfo\n    {\n        #region variables\n\n        DateTime _deletedOn;\n\n        #endregion\n\n        #region constructor\n\n        public HistoryRecordInfo()\n        { }\n\n        public HistoryRecordInfo(BinaryReader bR)\n            : base(bR)\n        { }\n\n        #endregion\n\n        #region static\n\n        public static HistoryRecordInfo ReadFrom(BinaryReader bR)\n        {\n            return new HistoryRecordInfo(bR);\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override void ReadRecordInfoFrom(BinaryReader bR)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    _deletedOn = bR.ReadDateTime();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"HistoryRecordInfo format version not supported.\");\n            }\n        }\n\n        protected override void WriteRecordInfoTo(BinaryWriter bW)\n        {\n            bW.Write((byte)1); //version\n\n            bW.Write(_deletedOn);\n        }\n\n        #endregion\n\n        #region properties\n\n        public DateTime DeletedOn\n        {\n            get { return _deletedOn; }\n            set { _deletedOn = value; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/NSRecordInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class NSRecordInfo : GenericRecordInfo\n    {\n        #region variables\n\n        IReadOnlyList<DnsResourceRecord> _glueRecords;\n\n        #endregion\n\n        #region constructor\n\n        public NSRecordInfo()\n        { }\n\n        public NSRecordInfo(BinaryReader bR)\n            : base(bR)\n        { }\n\n        #endregion\n\n        #region protected\n        \n        protected override void ReadExtendedRecordInfoFrom(BinaryReader bR)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 0: //no extended info\n                    break;\n\n                case 1:\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        DnsResourceRecord[] glueRecords = new DnsResourceRecord[count];\n\n                        for (int i = 0; i < glueRecords.Length; i++)\n                            glueRecords[i] = new DnsResourceRecord(bR.BaseStream);\n\n                        _glueRecords = glueRecords;\n                    }\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"NSRecordInfo format version not supported.\");\n            }\n        }\n\n        protected override void WriteExtendedRecordInfoTo(BinaryWriter bW)\n        {\n            bW.Write((byte)1); //version\n\n            if (_glueRecords is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(_glueRecords.Count));\n\n                foreach (DnsResourceRecord glueRecord in _glueRecords)\n                    glueRecord.WriteTo(bW.BaseStream);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyList<DnsResourceRecord> GlueRecords\n        {\n            get { return _glueRecords; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _glueRecords = null;\n                else\n                    _glueRecords = value;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/SOARecordInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.IO;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class SOARecordInfo : GenericRecordInfo\n    {\n        #region variables\n\n        byte _version;\n        bool _useSoaSerialDateScheme;\n\n        IReadOnlyList<NameServerAddress> _primaryNameServers; //depricated\n        DnsTransportProtocol _zoneTransferProtocol; //depricated\n        string _tsigKeyName = string.Empty; //depricated\n\n        #endregion\n\n        #region constructor\n\n        public SOARecordInfo()\n        { }\n\n        public SOARecordInfo(BinaryReader bR)\n            : base(bR)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ReadExtendedRecordInfoFrom(BinaryReader bR)\n        {\n            _version = bR.ReadByte();\n            switch (_version)\n            {\n                case 0: //no extended info\n                    break;\n\n                case 1:\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        NameServerAddress[] primaryNameServers = new NameServerAddress[count];\n\n                        for (int i = 0; i < primaryNameServers.Length; i++)\n                            primaryNameServers[i] = new NameServerAddress(bR);\n\n                        _primaryNameServers = primaryNameServers;\n                    }\n\n                    _zoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte();\n                    _tsigKeyName = bR.ReadShortString();\n                    _useSoaSerialDateScheme = bR.ReadBoolean();\n                    break;\n\n                case 2:\n                    _useSoaSerialDateScheme = bR.ReadBoolean();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"SOARecordInfo format version not supported.\");\n            }\n        }\n\n        protected override void WriteExtendedRecordInfoTo(BinaryWriter bW)\n        {\n            bW.Write((byte)2); //version\n\n            bW.Write(_useSoaSerialDateScheme);\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool Disabled\n        {\n            get { return base.Disabled; }\n            set\n            {\n                //cannot disable SOA            \n            }\n        }\n\n        public override uint ExpiryTtl\n        {\n            get { return base.ExpiryTtl; }\n            set\n            {\n                //cannot expire SOA\n            }\n        }\n\n        public byte Version\n        { get { return _version; } }\n\n        public bool UseSoaSerialDateScheme\n        {\n            get { return _useSoaSerialDateScheme; }\n            set { _useSoaSerialDateScheme = value; }\n        }\n\n        public IReadOnlyList<NameServerAddress> PrimaryNameServers\n        {\n            get { return _primaryNameServers; }\n            set { _primaryNameServers = value; }\n        }\n\n        public DnsTransportProtocol ZoneTransferProtocol\n        {\n            get { return _zoneTransferProtocol; }\n            set { _zoneTransferProtocol = value; }\n        }\n\n        public string TsigKeyName\n        {\n            get { return _tsigKeyName; }\n            set { _tsigKeyName = value; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ResourceRecords/SVCBRecordInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.IO;\n\nnamespace DnsServerCore.Dns.ResourceRecords\n{\n    class SVCBRecordInfo : GenericRecordInfo\n    {\n        #region variables\n\n        bool _autoIpv4Hint;\n        bool _autoIpv6Hint;\n\n        #endregion\n\n        #region constructor\n\n        public SVCBRecordInfo()\n        { }\n\n        public SVCBRecordInfo(BinaryReader bR)\n            : base(bR)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void ReadExtendedRecordInfoFrom(BinaryReader bR)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 0: //no extended info\n                    break;\n\n                case 1:\n                    _autoIpv4Hint = bR.ReadBoolean();\n                    _autoIpv6Hint = bR.ReadBoolean();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"SVCBRecordInfo format version not supported.\");\n            }\n        }\n\n        protected override void WriteExtendedRecordInfoTo(BinaryWriter bW)\n        {\n            bW.Write((byte)1); //version\n\n            bW.Write(_autoIpv4Hint);\n            bW.Write(_autoIpv6Hint);\n        }\n\n        #endregion\n\n        #region properties\n\n        public bool AutoIpv4Hint\n        {\n            get { return _autoIpv4Hint; }\n            set { _autoIpv4Hint = value; }\n        }\n\n        public bool AutoIpv6Hint\n        {\n            get { return _autoIpv6Hint; }\n            set { _autoIpv6Hint = value; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/StatsManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing DnsServerCore.HttpApi.Models;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Channels;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns\n{\n    public sealed class StatsManager : IDisposable\n    {\n        #region variables\n\n        const int DAILY_STATS_FILE_TOP_LIMIT = 1000;\n\n        readonly static HourlyStats _emptyHourlyStats = new HourlyStats();\n        readonly static StatCounter _emptyDailyStats = new StatCounter();\n\n        readonly DnsServer _dnsServer;\n        readonly string _statsFolder;\n\n        readonly StatCounter[] _lastHourStatCounters = new StatCounter[60];\n        readonly StatCounter[] _lastHourStatCountersCopy = new StatCounter[60];\n        readonly ConcurrentDictionary<DateTime, HourlyStats> _hourlyStatsCache = new ConcurrentDictionary<DateTime, HourlyStats>();\n        readonly ConcurrentDictionary<DateTime, StatCounter> _dailyStatsCache = new ConcurrentDictionary<DateTime, StatCounter>();\n\n        readonly Timer _maintenanceTimer;\n        const int MAINTENANCE_TIMER_INITIAL_INTERVAL = 10000;\n        const int MAINTENANCE_TIMER_PERIODIC_INTERVAL = 10000;\n\n        readonly Channel<StatsQueueItem> _channel;\n        readonly ChannelWriter<StatsQueueItem> _channelWriter;\n        readonly Thread _consumerThread;\n\n        readonly Timer _statsCleanupTimer;\n        const int STATS_CLEANUP_TIMER_INITIAL_INTERVAL = 60 * 1000;\n        const int STATS_CLEANUP_TIMER_PERIODIC_INTERVAL = 60 * 60 * 1000;\n\n        bool _enableInMemoryStats;\n        int _maxStatFileDays;\n\n        #endregion\n\n        #region constructor\n\n        static StatsManager()\n        {\n            _emptyDailyStats.Lock();\n        }\n\n        public StatsManager(DnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n            _statsFolder = Path.Combine(dnsServer.ConfigFolder, \"stats\");\n\n            if (!Directory.Exists(_statsFolder))\n                Directory.CreateDirectory(_statsFolder);\n\n            UnboundedChannelOptions options = new UnboundedChannelOptions();\n            options.SingleReader = true;\n\n            _channel = Channel.CreateUnbounded<StatsQueueItem>(options);\n            _channelWriter = _channel.Writer;\n\n            //load stats\n            LoadLastHourStats();\n\n            try\n            {\n                //do first maintenance\n                DoMaintenance();\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n\n            //start periodic maintenance timer\n            _maintenanceTimer = new Timer(delegate (object state)\n            {\n                try\n                {\n                    DoMaintenance();\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            }, null, MAINTENANCE_TIMER_INITIAL_INTERVAL, MAINTENANCE_TIMER_PERIODIC_INTERVAL);\n\n            //stats consumer thread\n            _consumerThread = new Thread(async delegate ()\n            {\n                try\n                {\n                    await foreach (StatsQueueItem item in _channel.Reader.ReadAllAsync())\n                    {\n                        if (_disposed)\n                            break;\n\n                        StatCounter statCounter = _lastHourStatCounters[item._timestamp.Minute];\n                        if (statCounter is not null)\n                        {\n                            DnsQuestionRecord query;\n\n                            if ((item._request is not null) && (item._request.Question.Count > 0))\n                                query = item._request.Question[0];\n                            else\n                                query = null;\n\n                            DnsServerResponseType responseType;\n\n                            if (item._response is null)\n                                responseType = DnsServerResponseType.Dropped;\n                            else if (item._response.Tag is null)\n                                responseType = DnsServerResponseType.Recursive;\n                            else\n                                responseType = (DnsServerResponseType)item._response.Tag;\n\n                            statCounter.Update(query, item._response is null ? DnsResponseCode.NoError : item._response.RCODE, responseType, item._remoteEP.Address, item._protocol, item._rateLimited);\n                        }\n\n                        if ((item._request is null) || (item._response is null))\n                            continue; //skip dropped requests for apps to prevent DoS\n\n                        foreach (IDnsQueryLogger logger in _dnsServer.DnsApplicationManager.DnsQueryLoggers)\n                        {\n                            try\n                            {\n                                _ = logger.InsertLogAsync(item._timestamp, item._request, item._remoteEP, item._protocol, item._response);\n                            }\n                            catch (Exception ex)\n                            {\n                                dnsServer.LogManager.Write(ex);\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            });\n\n            _consumerThread.Name = \"Stats\";\n            _consumerThread.IsBackground = true;\n            _consumerThread.Start();\n\n            _statsCleanupTimer = new Timer(delegate (object state)\n            {\n                try\n                {\n                    if (_maxStatFileDays < 1)\n                        return;\n\n                    DateTime cutoffDate = DateTime.UtcNow.AddDays(_maxStatFileDays * -1).Date;\n\n                    //delete hourly logs\n                    {\n                        string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, \"stats\"), \"*.stat\");\n\n                        foreach (string hourlyStatsFile in hourlyStatsFiles)\n                        {\n                            string hourlyStatsFileName = Path.GetFileNameWithoutExtension(hourlyStatsFile);\n\n                            if (!DateTime.TryParseExact(hourlyStatsFileName, \"yyyyMMddHH\", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime hourlyStatsFileDate))\n                                continue;\n\n                            if (hourlyStatsFileDate < cutoffDate)\n                            {\n                                try\n                                {\n                                    File.Delete(hourlyStatsFile);\n                                    dnsServer.LogManager.Write(\"StatsManager cleanup deleted the hourly stats file: \" + hourlyStatsFile);\n                                }\n                                catch (Exception ex)\n                                {\n                                    dnsServer.LogManager.Write(ex);\n                                }\n                            }\n                        }\n                    }\n\n                    //delete daily logs\n                    {\n                        string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, \"stats\"), \"*.dstat\");\n\n                        foreach (string dailyStatsFile in dailyStatsFiles)\n                        {\n                            string dailyStatsFileName = Path.GetFileNameWithoutExtension(dailyStatsFile);\n\n                            if (!DateTime.TryParseExact(dailyStatsFileName, \"yyyyMMdd\", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime dailyStatsFileDate))\n                                continue;\n\n                            if (dailyStatsFileDate < cutoffDate)\n                            {\n                                try\n                                {\n                                    File.Delete(dailyStatsFile);\n                                    dnsServer.LogManager.Write(\"StatsManager cleanup deleted the daily stats file: \" + dailyStatsFile);\n                                }\n                                catch (Exception ex)\n                                {\n                                    dnsServer.LogManager.Write(ex);\n                                }\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            });\n\n            _statsCleanupTimer.Change(STATS_CLEANUP_TIMER_INITIAL_INTERVAL, STATS_CLEANUP_TIMER_PERIODIC_INTERVAL);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _maintenanceTimer?.Dispose();\n            _statsCleanupTimer?.Dispose();\n\n            _channelWriter?.TryComplete();\n\n            DoMaintenance(); //do last maintenance\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private void LoadLastHourStats()\n        {\n            try\n            {\n                DateTime currentDateTime = DateTime.UtcNow;\n                DateTime lastHourDateTime = currentDateTime.AddMinutes(-60);\n\n                HourlyStats lastHourlyStats = null;\n                DateTime lastHourlyStatsDateTime = new DateTime();\n\n                for (int i = 0; i < 60; i++)\n                {\n                    DateTime lastDateTime = lastHourDateTime.AddMinutes(i);\n\n                    if ((lastHourlyStats == null) || (lastDateTime.Hour != lastHourlyStatsDateTime.Hour))\n                    {\n                        lastHourlyStats = LoadHourlyStats(lastDateTime);\n                        lastHourlyStatsDateTime = lastDateTime;\n                    }\n\n                    _lastHourStatCounters[lastDateTime.Minute] = lastHourlyStats.MinuteStats[lastDateTime.Minute];\n                    _lastHourStatCountersCopy[lastDateTime.Minute] = _lastHourStatCounters[lastDateTime.Minute];\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n        }\n\n        private void DoMaintenance()\n        {\n            //load new stats counter 5 min ahead of current time\n            DateTime currentDateTime = DateTime.UtcNow;\n\n            for (int i = 0; i < 5; i++)\n            {\n                int minute = currentDateTime.AddMinutes(i).Minute;\n\n                StatCounter statCounter = _lastHourStatCounters[minute];\n                if ((statCounter == null) || statCounter.IsLocked)\n                    _lastHourStatCounters[minute] = new StatCounter();\n            }\n\n            //save data upto last 5 mins\n            DateTime last5MinDateTime = currentDateTime.AddMinutes(-5);\n\n            for (int i = 0; i < 5; i++)\n            {\n                DateTime lastDateTime = last5MinDateTime.AddMinutes(i);\n\n                StatCounter lastStatCounter = _lastHourStatCounters[lastDateTime.Minute];\n                if ((lastStatCounter != null) && !lastStatCounter.IsLocked)\n                {\n                    lastStatCounter.Lock();\n\n                    if (!_enableInMemoryStats)\n                    {\n                        //load hourly stats data\n                        HourlyStats hourlyStats = LoadHourlyStats(lastDateTime);\n\n                        //update hourly stats file\n                        hourlyStats.UpdateStat(lastDateTime, lastStatCounter);\n\n                        //save hourly stats\n                        SaveHourlyStats(lastDateTime, hourlyStats);\n                    }\n\n                    //keep copy for api\n                    _lastHourStatCountersCopy[lastDateTime.Minute] = lastStatCounter;\n                }\n            }\n\n            //load previous day stats to auto create daily stats file\n            LoadDailyStats(currentDateTime.AddDays(-1));\n\n            //remove old data from hourly stats cache\n            {\n                DateTime threshold = DateTime.UtcNow.AddHours(-24);\n                threshold = new DateTime(threshold.Year, threshold.Month, threshold.Day, threshold.Hour, 0, 0, DateTimeKind.Utc);\n\n                List<DateTime> _keysToRemove = new List<DateTime>();\n\n                foreach (KeyValuePair<DateTime, HourlyStats> item in _hourlyStatsCache)\n                {\n                    if (item.Key < threshold)\n                        _keysToRemove.Add(item.Key);\n                }\n\n                foreach (DateTime key in _keysToRemove)\n                    _hourlyStatsCache.TryRemove(key, out _);\n            }\n\n            //unload minute stats data from hourly stats cache for data older than last hour\n            {\n                DateTime lastHourThreshold = DateTime.UtcNow.AddHours(-1);\n                lastHourThreshold = new DateTime(lastHourThreshold.Year, lastHourThreshold.Month, lastHourThreshold.Day, lastHourThreshold.Hour, 0, 0, DateTimeKind.Utc);\n\n                foreach (KeyValuePair<DateTime, HourlyStats> item in _hourlyStatsCache)\n                {\n                    if (item.Key < lastHourThreshold)\n                        item.Value.UnloadMinuteStats();\n                }\n            }\n\n            //remove old data from daily stats cache\n            {\n                DateTime threshold = DateTime.UtcNow.AddMonths(-12);\n                threshold = new DateTime(threshold.Year, threshold.Month, 1, 0, 0, 0, DateTimeKind.Utc);\n\n                List<DateTime> _keysToRemove = new List<DateTime>();\n\n                foreach (KeyValuePair<DateTime, StatCounter> item in _dailyStatsCache)\n                {\n                    if (item.Key < threshold)\n                        _keysToRemove.Add(item.Key);\n                }\n\n                foreach (DateTime key in _keysToRemove)\n                    _dailyStatsCache.TryRemove(key, out _);\n            }\n        }\n\n        private HourlyStats LoadHourlyStats(DateTime dateTime, bool forceReload = false, bool ifNotExistsReturnEmptyHourlyStats = false)\n        {\n            if (_enableInMemoryStats)\n                return _emptyHourlyStats;\n\n            DateTime hourlyDateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, 0, 0, 0, DateTimeKind.Utc);\n\n            if (forceReload || !_hourlyStatsCache.TryGetValue(hourlyDateTime, out HourlyStats hourlyStats))\n            {\n                string hourlyStatsFile = Path.Combine(_statsFolder, dateTime.ToString(\"yyyyMMddHH\") + \".stat\");\n\n                if (File.Exists(hourlyStatsFile))\n                {\n                    try\n                    {\n                        using (FileStream fS = new FileStream(hourlyStatsFile, FileMode.Open, FileAccess.Read))\n                        {\n                            hourlyStats = new HourlyStats(new BinaryReader(fS));\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n\n                        if (ifNotExistsReturnEmptyHourlyStats)\n                            hourlyStats = _emptyHourlyStats;\n                        else\n                            hourlyStats = new HourlyStats();\n                    }\n                }\n                else\n                {\n                    if (ifNotExistsReturnEmptyHourlyStats)\n                        hourlyStats = _emptyHourlyStats;\n                    else\n                        hourlyStats = new HourlyStats();\n                }\n\n                _hourlyStatsCache[hourlyDateTime] = hourlyStats;\n            }\n\n            return hourlyStats;\n        }\n\n        private StatCounter LoadDailyStats(DateTime dateTime)\n        {\n            if (_enableInMemoryStats)\n                return _emptyDailyStats;\n\n            DateTime dailyDateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 0, 0, 0, 0, DateTimeKind.Utc);\n\n            if (!_dailyStatsCache.TryGetValue(dailyDateTime, out StatCounter dailyStats))\n            {\n                string dailyStatsFile = Path.Combine(_statsFolder, dateTime.ToString(\"yyyyMMdd\") + \".dstat\");\n\n                if (File.Exists(dailyStatsFile))\n                {\n                    try\n                    {\n                        using (FileStream fS = new FileStream(dailyStatsFile, FileMode.Open, FileAccess.Read))\n                        {\n                            dailyStats = new StatCounter(new BinaryReader(fS));\n                        }\n\n                        //check if existing file could be truncated to avoid loading unnecessary data in memory\n                        if (dailyStats.Truncate(DAILY_STATS_FILE_TOP_LIMIT))\n                        {\n                            SaveDailyStats(dailyDateTime, dailyStats); //save truncated file\n                            GC.Collect();\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                }\n\n                if (dailyStats is null)\n                {\n                    dailyStats = new StatCounter();\n                    dailyStats.Lock();\n\n                    for (int hour = 0; hour < 24; hour++) //hours\n                    {\n                        HourlyStats hourlyStats = LoadHourlyStats(dailyDateTime.AddHours(hour), ifNotExistsReturnEmptyHourlyStats: true);\n                        dailyStats.Merge(hourlyStats.HourStat);\n                    }\n\n                    if (dailyStats.TotalQueries > 0)\n                    {\n                        _ = dailyStats.Truncate(DAILY_STATS_FILE_TOP_LIMIT);\n                        SaveDailyStats(dailyDateTime, dailyStats);\n                        GC.Collect();\n                    }\n                }\n\n                if (!_dailyStatsCache.TryAdd(dailyDateTime, dailyStats))\n                {\n                    if (!_dailyStatsCache.TryGetValue(dailyDateTime, out dailyStats))\n                        throw new DnsServerException(\"Unable to load daily stats.\");\n                }\n            }\n\n            return dailyStats;\n        }\n\n        private void SaveHourlyStats(DateTime dateTime, HourlyStats hourlyStats)\n        {\n            string hourlyStatsFile = Path.Combine(_statsFolder, dateTime.ToString(\"yyyyMMddHH\") + \".stat\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(hourlyStatsFile, FileMode.Create, FileAccess.Write))\n                {\n                    hourlyStats.WriteTo(new BinaryWriter(fS));\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n        }\n\n        private void SaveDailyStats(DateTime dateTime, StatCounter dailyStats)\n        {\n            string dailyStatsFile = Path.Combine(_statsFolder, dateTime.ToString(\"yyyyMMdd\") + \".dstat\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(dailyStatsFile, FileMode.Create, FileAccess.Write))\n                {\n                    dailyStats.WriteTo(new BinaryWriter(fS));\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n        }\n\n        private void Flush()\n        {\n            //clear in memory stats\n            for (int i = 0; i < _lastHourStatCountersCopy.Length; i++)\n                _lastHourStatCountersCopy[i] = null;\n\n            _hourlyStatsCache.Clear();\n            _dailyStatsCache.Clear();\n        }\n\n        #endregion\n\n        #region public\n\n        public void ReloadStats()\n        {\n            Flush();\n            LoadLastHourStats();\n        }\n\n        public void DeleteAllStats()\n        {\n            foreach (string hourlyStatsFile in Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, \"stats\"), \"*.stat\", SearchOption.TopDirectoryOnly))\n            {\n                File.Delete(hourlyStatsFile);\n            }\n\n            foreach (string dailyStatsFile in Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, \"stats\"), \"*.dstat\", SearchOption.TopDirectoryOnly))\n            {\n                File.Delete(dailyStatsFile);\n            }\n\n            Flush();\n        }\n\n        public void QueueUpdate(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response, bool rateLimited)\n        {\n            _channelWriter.TryWrite(new StatsQueueItem(request, remoteEP, protocol, response, rateLimited));\n        }\n\n        public DashboardStats GetLastHourMinuteWiseStats(bool utcFormat)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            string[] labels = new string[60];\n\n            long[] totalQueriesPerInterval = new long[60];\n            long[] totalNoErrorPerInterval = new long[60];\n            long[] totalServerFailurePerInterval = new long[60];\n            long[] totalNxDomainPerInterval = new long[60];\n            long[] totalRefusedPerInterval = new long[60];\n\n            long[] totalAuthHitPerInterval = new long[60];\n            long[] totalRecursionsPerInterval = new long[60];\n            long[] totalCacheHitPerInterval = new long[60];\n            long[] totalBlockedPerInterval = new long[60];\n            long[] totalDroppedPerInterval = new long[60];\n\n            long[] totalClientsPerInterval = new long[60];\n\n            DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(-60);\n            lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc);\n\n            for (int minute = 0; minute < 60; minute++)\n            {\n                DateTime lastDateTime = lastHourDateTime.AddMinutes(minute);\n                string label;\n\n                if (utcFormat)\n                    label = lastDateTime.AddMinutes(1).ToString(\"O\");\n                else\n                    label = lastDateTime.AddMinutes(1).ToLocalTime().ToString(\"HH:mm\");\n\n                labels[minute] = label;\n\n                StatCounter statCounter = _lastHourStatCountersCopy[lastDateTime.Minute];\n                if ((statCounter != null) && statCounter.IsLocked)\n                {\n                    totalStatCounter.Merge(statCounter);\n\n                    totalQueriesPerInterval[minute] = statCounter.TotalQueries;\n\n                    totalNoErrorPerInterval[minute] = statCounter.TotalNoError;\n                    totalServerFailurePerInterval[minute] = statCounter.TotalServerFailure;\n                    totalNxDomainPerInterval[minute] = statCounter.TotalNxDomain;\n                    totalRefusedPerInterval[minute] = statCounter.TotalRefused;\n\n                    totalAuthHitPerInterval[minute] = statCounter.TotalAuthoritative;\n                    totalRecursionsPerInterval[minute] = statCounter.TotalRecursive;\n                    totalCacheHitPerInterval[minute] = statCounter.TotalCached;\n                    totalBlockedPerInterval[minute] = statCounter.TotalBlocked;\n                    totalDroppedPerInterval[minute] = statCounter.TotalDropped;\n\n                    totalClientsPerInterval[minute] = statCounter.TotalClients;\n                }\n            }\n\n            DashboardStats.ChartData mainChartData = new DashboardStats.ChartData()\n            {\n                Labels = labels,\n                DataSets =\n                [\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Total\",\n                        Data = totalQueriesPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"No Error\",\n                        Data = totalNoErrorPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Server Failure\",\n                        Data = totalServerFailurePerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"NX Domain\",\n                        Data = totalNxDomainPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Refused\",\n                        Data = totalRefusedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Authoritative\",\n                        Data = totalAuthHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Recursive\",\n                        Data = totalRecursionsPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Cached\",\n                        Data = totalCacheHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Blocked\",\n                        Data = totalBlockedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Dropped\",\n                        Data = totalDroppedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Clients\",\n                        Data = totalClientsPerInterval\n                    }\n                ]\n            };\n\n            return new DashboardStats()\n            {\n                Stats = totalStatCounter.GetStatsData(),\n                MainChartData = mainChartData,\n                QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(),\n                QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(),\n                ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(),\n                TopClients = totalStatCounter.GetTopClientStats(10),\n                TopDomains = totalStatCounter.GetTopDomainStats(10),\n                TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10)\n            };\n        }\n\n        public DashboardStats GetLastDayHourWiseStats(bool utcFormat)\n        {\n            return GetHourWiseStats(DateTime.UtcNow.AddHours(-24), 24, utcFormat);\n        }\n\n        public DashboardStats GetLastWeekDayWiseStats(bool utcFormat)\n        {\n            return GetDayWiseStats(DateTime.UtcNow.AddDays(-7).Date, 7, utcFormat);\n        }\n\n        public DashboardStats GetLastMonthDayWiseStats(bool utcFormat)\n        {\n            return GetDayWiseStats(DateTime.UtcNow.AddDays(-31).Date, 31, utcFormat);\n        }\n\n        public DashboardStats GetLastYearMonthWiseStats(bool utcFormat)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            string[] labels = new string[12];\n\n            long[] totalQueriesPerInterval = new long[12];\n            long[] totalNoErrorPerInterval = new long[12];\n            long[] totalServerFailurePerInterval = new long[12];\n            long[] totalNxDomainPerInterval = new long[12];\n            long[] totalRefusedPerInterval = new long[12];\n\n            long[] totalAuthHitPerInterval = new long[12];\n            long[] totalRecursionsPerInterval = new long[12];\n            long[] totalCacheHitPerInterval = new long[12];\n            long[] totalBlockedPerInterval = new long[12];\n            long[] totalDroppedPerInterval = new long[12];\n\n            long[] totalClientsPerInterval = new long[12];\n\n            DateTime lastYearDateTime = DateTime.UtcNow.AddMonths(-12);\n            lastYearDateTime = new DateTime(lastYearDateTime.Year, lastYearDateTime.Month, 1, 0, 0, 0, DateTimeKind.Utc);\n\n            for (int month = 0; month < 12; month++) //months\n            {\n                StatCounter monthlyStatCounter = new StatCounter();\n                monthlyStatCounter.Lock();\n\n                DateTime lastMonthDateTime = lastYearDateTime.AddMonths(month);\n                string label;\n\n                if (utcFormat)\n                    label = lastMonthDateTime.ToString(\"O\");\n                else\n                    label = lastMonthDateTime.ToLocalTime().ToString(\"MM/yyyy\");\n\n                labels[month] = label;\n\n                int days = DateTime.DaysInMonth(lastMonthDateTime.Year, lastMonthDateTime.Month);\n\n                for (int day = 0; day < days; day++) //days\n                {\n                    StatCounter dailyStatCounter = LoadDailyStats(lastMonthDateTime.AddDays(day));\n                    monthlyStatCounter.Merge(dailyStatCounter, true);\n                }\n\n                totalStatCounter.Merge(monthlyStatCounter, true);\n\n                totalQueriesPerInterval[month] = monthlyStatCounter.TotalQueries;\n\n                totalNoErrorPerInterval[month] = monthlyStatCounter.TotalNoError;\n                totalServerFailurePerInterval[month] = monthlyStatCounter.TotalServerFailure;\n                totalNxDomainPerInterval[month] = monthlyStatCounter.TotalNxDomain;\n                totalRefusedPerInterval[month] = monthlyStatCounter.TotalRefused;\n\n                totalAuthHitPerInterval[month] = monthlyStatCounter.TotalAuthoritative;\n                totalRecursionsPerInterval[month] = monthlyStatCounter.TotalRecursive;\n                totalCacheHitPerInterval[month] = monthlyStatCounter.TotalCached;\n                totalBlockedPerInterval[month] = monthlyStatCounter.TotalBlocked;\n                totalDroppedPerInterval[month] = monthlyStatCounter.TotalDropped;\n\n                totalClientsPerInterval[month] = monthlyStatCounter.TotalClients;\n            }\n\n            DashboardStats.ChartData mainChartData = new DashboardStats.ChartData()\n            {\n                Labels = labels,\n                DataSets =\n                [\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Total\",\n                        Data = totalQueriesPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"No Error\",\n                        Data = totalNoErrorPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Server Failure\",\n                        Data = totalServerFailurePerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"NX Domain\",\n                        Data = totalNxDomainPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Refused\",\n                        Data = totalRefusedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Authoritative\",\n                        Data = totalAuthHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Recursive\",\n                        Data = totalRecursionsPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Cached\",\n                        Data = totalCacheHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Blocked\",\n                        Data = totalBlockedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Dropped\",\n                        Data = totalDroppedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Clients\",\n                        Data = totalClientsPerInterval\n                    }\n                ]\n            };\n\n            return new DashboardStats()\n            {\n                Stats = totalStatCounter.GetStatsData(),\n                MainChartData = mainChartData,\n                QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(),\n                QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(),\n                ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(),\n                TopClients = totalStatCounter.GetTopClientStats(10),\n                TopDomains = totalStatCounter.GetTopDomainStats(10),\n                TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10)\n            };\n        }\n\n        public DashboardStats GetMinuteWiseStats(DateTime startDate, DateTime endDate, bool utcFormat)\n        {\n            return GetMinuteWiseStats(startDate, Convert.ToInt32((endDate - startDate).TotalMinutes) + 1, utcFormat);\n        }\n\n        public DashboardStats GetMinuteWiseStats(DateTime startDate, int minutes, bool utcFormat)\n        {\n            startDate = startDate.AddMinutes(-1);\n\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            string[] labels = new string[minutes];\n\n            long[] totalQueriesPerInterval = new long[minutes];\n            long[] totalNoErrorPerInterval = new long[minutes];\n            long[] totalServerFailurePerInterval = new long[minutes];\n            long[] totalNxDomainPerInterval = new long[minutes];\n            long[] totalRefusedPerInterval = new long[minutes];\n\n            long[] totalAuthHitPerInterval = new long[minutes];\n            long[] totalRecursionsPerInterval = new long[minutes];\n            long[] totalCacheHitPerInterval = new long[minutes];\n            long[] totalBlockedPerInterval = new long[minutes];\n            long[] totalDroppedPerInterval = new long[minutes];\n\n            long[] totalClientsPerInterval = new long[minutes];\n\n            for (int minute = 0; minute < minutes; minute++)\n            {\n                DateTime lastDateTime = startDate.AddMinutes(minute);\n\n                HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true);\n                if (hourlyStats.MinuteStats is null)\n                    hourlyStats = LoadHourlyStats(lastDateTime, true);\n\n                StatCounter minuteStatCounter = hourlyStats.MinuteStats[lastDateTime.Minute];\n\n                string label;\n\n                if (utcFormat)\n                    label = lastDateTime.AddMinutes(1).ToString(\"O\");\n                else\n                    label = lastDateTime.AddMinutes(1).ToLocalTime().ToString(\"MM/dd HH:mm\");\n\n                labels[minute] = label;\n\n                totalStatCounter.Merge(minuteStatCounter);\n\n                totalQueriesPerInterval[minute] = minuteStatCounter.TotalQueries;\n\n                totalNoErrorPerInterval[minute] = minuteStatCounter.TotalNoError;\n                totalServerFailurePerInterval[minute] = minuteStatCounter.TotalServerFailure;\n                totalNxDomainPerInterval[minute] = minuteStatCounter.TotalNxDomain;\n                totalRefusedPerInterval[minute] = minuteStatCounter.TotalRefused;\n\n                totalAuthHitPerInterval[minute] = minuteStatCounter.TotalAuthoritative;\n                totalRecursionsPerInterval[minute] = minuteStatCounter.TotalRecursive;\n                totalCacheHitPerInterval[minute] = minuteStatCounter.TotalCached;\n                totalBlockedPerInterval[minute] = minuteStatCounter.TotalBlocked;\n                totalDroppedPerInterval[minute] = minuteStatCounter.TotalDropped;\n\n                totalClientsPerInterval[minute] = minuteStatCounter.TotalClients;\n            }\n\n            DashboardStats.ChartData mainChartData = new DashboardStats.ChartData()\n            {\n                Labels = labels,\n                DataSets =\n                [\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Total\",\n                        Data = totalQueriesPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"No Error\",\n                        Data = totalNoErrorPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Server Failure\",\n                        Data = totalServerFailurePerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"NX Domain\",\n                        Data = totalNxDomainPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Refused\",\n                        Data = totalRefusedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Authoritative\",\n                        Data = totalAuthHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Recursive\",\n                        Data = totalRecursionsPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Cached\",\n                        Data = totalCacheHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Blocked\",\n                        Data = totalBlockedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Dropped\",\n                        Data = totalDroppedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Clients\",\n                        Data = totalClientsPerInterval\n                    }\n                ]\n            };\n\n            return new DashboardStats()\n            {\n                Stats = totalStatCounter.GetStatsData(),\n                MainChartData = mainChartData,\n                QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(),\n                QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(),\n                ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(),\n                TopClients = totalStatCounter.GetTopClientStats(10),\n                TopDomains = totalStatCounter.GetTopDomainStats(10),\n                TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10)\n            };\n        }\n\n        public DashboardStats GetHourWiseStats(DateTime startDate, DateTime endDate, bool utcFormat)\n        {\n            return GetHourWiseStats(startDate, Convert.ToInt32((endDate - startDate).TotalHours) + 1, utcFormat);\n        }\n\n        public DashboardStats GetHourWiseStats(DateTime startDate, int hours, bool utcFormat)\n        {\n            startDate = new DateTime(startDate.Year, startDate.Month, startDate.Day, startDate.Hour, 0, 0, 0, DateTimeKind.Utc);\n\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            string[] labels = new string[hours];\n\n            long[] totalQueriesPerInterval = new long[hours];\n            long[] totalNoErrorPerInterval = new long[hours];\n            long[] totalServerFailurePerInterval = new long[hours];\n            long[] totalNxDomainPerInterval = new long[hours];\n            long[] totalRefusedPerInterval = new long[hours];\n\n            long[] totalAuthHitPerInterval = new long[hours];\n            long[] totalRecursionsPerInterval = new long[hours];\n            long[] totalCacheHitPerInterval = new long[hours];\n            long[] totalBlockedPerInterval = new long[hours];\n            long[] totalDroppedPerInterval = new long[hours];\n\n            long[] totalClientsPerInterval = new long[hours];\n\n            for (int hour = 0; hour < hours; hour++)\n            {\n                DateTime lastDateTime = startDate.AddHours(hour);\n                string label;\n\n                if (utcFormat)\n                    label = lastDateTime.AddHours(1).ToString(\"O\");\n                else\n                    label = lastDateTime.AddHours(1).ToLocalTime().ToString(\"MM/dd HH\") + \":00\";\n\n                labels[hour] = label;\n\n                HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true);\n                StatCounter hourlyStatCounter = hourlyStats.HourStat;\n\n                totalStatCounter.Merge(hourlyStatCounter);\n\n                totalQueriesPerInterval[hour] = hourlyStatCounter.TotalQueries;\n\n                totalNoErrorPerInterval[hour] = hourlyStatCounter.TotalNoError;\n                totalServerFailurePerInterval[hour] = hourlyStatCounter.TotalServerFailure;\n                totalNxDomainPerInterval[hour] = hourlyStatCounter.TotalNxDomain;\n                totalRefusedPerInterval[hour] = hourlyStatCounter.TotalRefused;\n\n                totalAuthHitPerInterval[hour] = hourlyStatCounter.TotalAuthoritative;\n                totalRecursionsPerInterval[hour] = hourlyStatCounter.TotalRecursive;\n                totalCacheHitPerInterval[hour] = hourlyStatCounter.TotalCached;\n                totalBlockedPerInterval[hour] = hourlyStatCounter.TotalBlocked;\n                totalDroppedPerInterval[hour] = hourlyStatCounter.TotalDropped;\n\n                totalClientsPerInterval[hour] = hourlyStatCounter.TotalClients;\n            }\n\n            DashboardStats.ChartData mainChartData = new DashboardStats.ChartData()\n            {\n                Labels = labels,\n                DataSets =\n                [\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Total\",\n                        Data = totalQueriesPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"No Error\",\n                        Data = totalNoErrorPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Server Failure\",\n                        Data = totalServerFailurePerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"NX Domain\",\n                        Data = totalNxDomainPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Refused\",\n                        Data = totalRefusedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Authoritative\",\n                        Data = totalAuthHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Recursive\",\n                        Data = totalRecursionsPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Cached\",\n                        Data = totalCacheHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Blocked\",\n                        Data = totalBlockedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Dropped\",\n                        Data = totalDroppedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Clients\",\n                        Data = totalClientsPerInterval\n                    }\n                ]\n            };\n\n            return new DashboardStats()\n            {\n                Stats = totalStatCounter.GetStatsData(),\n                MainChartData = mainChartData,\n                QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(),\n                QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(),\n                ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(),\n                TopClients = totalStatCounter.GetTopClientStats(10),\n                TopDomains = totalStatCounter.GetTopDomainStats(10),\n                TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10)\n            };\n        }\n\n        public DashboardStats GetDayWiseStats(DateTime startDate, DateTime endDate, bool utcFormat)\n        {\n            return GetDayWiseStats(startDate, Convert.ToInt32((endDate - startDate).TotalDays) + 1, utcFormat);\n        }\n\n        public DashboardStats GetDayWiseStats(DateTime startDate, int days, bool utcFormat)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            string[] labels = new string[days];\n\n            long[] totalQueriesPerInterval = new long[days];\n            long[] totalNoErrorPerInterval = new long[days];\n            long[] totalServerFailurePerInterval = new long[days];\n            long[] totalNxDomainPerInterval = new long[days];\n            long[] totalRefusedPerInterval = new long[days];\n\n            long[] totalAuthHitPerInterval = new long[days];\n            long[] totalRecursionsPerInterval = new long[days];\n            long[] totalCacheHitPerInterval = new long[days];\n            long[] totalBlockedPerInterval = new long[days];\n            long[] totalDroppedPerInterval = new long[days];\n\n            long[] totalClientsPerInterval = new long[days];\n\n            for (int day = 0; day < days; day++) //days\n            {\n                DateTime lastDayDateTime = startDate.AddDays(day);\n                string label;\n\n                if (utcFormat)\n                    label = lastDayDateTime.ToString(\"O\");\n                else\n                    label = lastDayDateTime.ToLocalTime().ToString(\"MM/dd\");\n\n                labels[day] = label;\n\n                StatCounter dailyStatCounter = LoadDailyStats(lastDayDateTime);\n                totalStatCounter.Merge(dailyStatCounter, true);\n\n                totalQueriesPerInterval[day] = dailyStatCounter.TotalQueries;\n\n                totalNoErrorPerInterval[day] = dailyStatCounter.TotalNoError;\n                totalServerFailurePerInterval[day] = dailyStatCounter.TotalServerFailure;\n                totalNxDomainPerInterval[day] = dailyStatCounter.TotalNxDomain;\n                totalRefusedPerInterval[day] = dailyStatCounter.TotalRefused;\n\n                totalAuthHitPerInterval[day] = dailyStatCounter.TotalAuthoritative;\n                totalRecursionsPerInterval[day] = dailyStatCounter.TotalRecursive;\n                totalCacheHitPerInterval[day] = dailyStatCounter.TotalCached;\n                totalBlockedPerInterval[day] = dailyStatCounter.TotalBlocked;\n                totalDroppedPerInterval[day] = dailyStatCounter.TotalDropped;\n\n                totalClientsPerInterval[day] = dailyStatCounter.TotalClients;\n            }\n\n            DashboardStats.ChartData mainChartData = new DashboardStats.ChartData()\n            {\n                Labels = labels,\n                DataSets =\n                [\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Total\",\n                        Data = totalQueriesPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"No Error\",\n                        Data = totalNoErrorPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Server Failure\",\n                        Data = totalServerFailurePerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"NX Domain\",\n                        Data = totalNxDomainPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Refused\",\n                        Data = totalRefusedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Authoritative\",\n                        Data = totalAuthHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Recursive\",\n                        Data = totalRecursionsPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Cached\",\n                        Data = totalCacheHitPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Blocked\",\n                        Data = totalBlockedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Dropped\",\n                        Data = totalDroppedPerInterval\n                    },\n                    new DashboardStats.DataSet()\n                    {\n                        Label = \"Clients\",\n                        Data = totalClientsPerInterval\n                    }\n                ]\n            };\n\n            return new DashboardStats()\n            {\n                Stats = totalStatCounter.GetStatsData(),\n                MainChartData = mainChartData,\n                QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(),\n                QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(),\n                ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(),\n                TopClients = totalStatCounter.GetTopClientStats(10),\n                TopDomains = totalStatCounter.GetTopDomainStats(10),\n                TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10)\n            };\n        }\n\n        public DashboardStats GetLastHourTopStats(DashboardTopStatsType type, int limit)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(-60);\n            lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc);\n\n            for (int minute = 0; minute < 60; minute++)\n            {\n                DateTime lastDateTime = lastHourDateTime.AddMinutes(minute);\n\n                StatCounter statCounter = _lastHourStatCountersCopy[lastDateTime.Minute];\n                if ((statCounter != null) && statCounter.IsLocked)\n                    totalStatCounter.Merge(statCounter);\n            }\n\n            switch (type)\n            {\n                case DashboardTopStatsType.TopClients:\n                    return new DashboardStats()\n                    {\n                        TopClients = totalStatCounter.GetTopClientStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopDomains:\n                    return new DashboardStats()\n                    {\n                        TopDomains = totalStatCounter.GetTopDomainStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopBlockedDomains:\n                    return new DashboardStats()\n                    {\n                        TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit)\n                    };\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public DashboardStats GetLastDayTopStats(DashboardTopStatsType type, int limit)\n        {\n            return GetHourWiseTopStats(DateTime.UtcNow.AddHours(-24), 24, type, limit);\n        }\n\n        public DashboardStats GetLastWeekTopStats(DashboardTopStatsType type, int limit)\n        {\n            return GetDayWiseTopStats(DateTime.UtcNow.AddDays(-7).Date, 7, type, limit);\n        }\n\n        public DashboardStats GetLastMonthTopStats(DashboardTopStatsType type, int limit)\n        {\n            return GetDayWiseTopStats(DateTime.UtcNow.AddDays(-31).Date, 31, type, limit);\n        }\n\n        public DashboardStats GetLastYearTopStats(DashboardTopStatsType type, int limit)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            DateTime lastYearDateTime = DateTime.UtcNow.AddMonths(-12);\n            lastYearDateTime = new DateTime(lastYearDateTime.Year, lastYearDateTime.Month, 1, 0, 0, 0, DateTimeKind.Utc);\n\n            for (int month = 0; month < 12; month++) //months\n            {\n                StatCounter monthlyStatCounter = new StatCounter();\n                monthlyStatCounter.Lock();\n\n                DateTime lastMonthDateTime = lastYearDateTime.AddMonths(month);\n\n                int days = DateTime.DaysInMonth(lastMonthDateTime.Year, lastMonthDateTime.Month);\n\n                for (int day = 0; day < days; day++) //days\n                {\n                    StatCounter dailyStatCounter = LoadDailyStats(lastMonthDateTime.AddDays(day));\n                    monthlyStatCounter.Merge(dailyStatCounter, true);\n                }\n\n                totalStatCounter.Merge(monthlyStatCounter, true);\n            }\n\n            switch (type)\n            {\n                case DashboardTopStatsType.TopClients:\n                    return new DashboardStats()\n                    {\n                        TopClients = totalStatCounter.GetTopClientStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopDomains:\n                    return new DashboardStats()\n                    {\n                        TopDomains = totalStatCounter.GetTopDomainStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopBlockedDomains:\n                    return new DashboardStats()\n                    {\n                        TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit)\n                    };\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public DashboardStats GetMinuteWiseTopStats(DateTime startDate, DateTime endDate, DashboardTopStatsType type, int limit)\n        {\n            return GetMinuteWiseTopStats(startDate, Convert.ToInt32((endDate - startDate).TotalMinutes) + 1, type, limit);\n        }\n\n        public DashboardStats GetMinuteWiseTopStats(DateTime startDate, int minutes, DashboardTopStatsType type, int limit)\n        {\n            startDate = startDate.AddMinutes(-1);\n\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            for (int minute = 0; minute < minutes; minute++)\n            {\n                DateTime lastDateTime = startDate.AddMinutes(minute);\n\n                HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true);\n                if (hourlyStats.MinuteStats is null)\n                    hourlyStats = LoadHourlyStats(lastDateTime, true);\n\n                StatCounter minuteStatCounter = hourlyStats.MinuteStats[lastDateTime.Minute];\n\n                totalStatCounter.Merge(minuteStatCounter);\n            }\n\n            switch (type)\n            {\n                case DashboardTopStatsType.TopClients:\n                    return new DashboardStats()\n                    {\n                        TopClients = totalStatCounter.GetTopClientStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopDomains:\n                    return new DashboardStats()\n                    {\n                        TopDomains = totalStatCounter.GetTopDomainStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopBlockedDomains:\n                    return new DashboardStats()\n                    {\n                        TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit)\n                    };\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public DashboardStats GetHourWiseTopStats(DateTime startDate, DateTime endDate, DashboardTopStatsType type, int limit)\n        {\n            return GetHourWiseTopStats(startDate, Convert.ToInt32((endDate - startDate).TotalHours) + 1, type, limit);\n        }\n\n        public DashboardStats GetHourWiseTopStats(DateTime startDate, int hours, DashboardTopStatsType type, int limit)\n        {\n            startDate = new DateTime(startDate.Year, startDate.Month, startDate.Day, startDate.Hour, 0, 0, 0, DateTimeKind.Utc);\n\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            for (int hour = 0; hour < hours; hour++)\n            {\n                DateTime lastDateTime = startDate.AddHours(hour);\n\n                HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true);\n                StatCounter hourlyStatCounter = hourlyStats.HourStat;\n\n                totalStatCounter.Merge(hourlyStatCounter);\n            }\n\n            switch (type)\n            {\n                case DashboardTopStatsType.TopClients:\n                    return new DashboardStats()\n                    {\n                        TopClients = totalStatCounter.GetTopClientStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopDomains:\n                    return new DashboardStats()\n                    {\n                        TopDomains = totalStatCounter.GetTopDomainStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopBlockedDomains:\n                    return new DashboardStats()\n                    {\n                        TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit)\n                    };\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public DashboardStats GetDayWiseTopStats(DateTime startDate, DateTime endDate, DashboardTopStatsType type, int limit)\n        {\n            return GetDayWiseTopStats(startDate, Convert.ToInt32((endDate - startDate).TotalDays) + 1, type, limit);\n        }\n\n        public DashboardStats GetDayWiseTopStats(DateTime startDate, int days, DashboardTopStatsType type, int limit)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            for (int day = 0; day < days; day++) //days\n            {\n                DateTime lastDayDateTime = startDate.AddDays(day);\n\n                StatCounter dailyStatCounter = LoadDailyStats(lastDayDateTime);\n                totalStatCounter.Merge(dailyStatCounter, true);\n            }\n\n            switch (type)\n            {\n                case DashboardTopStatsType.TopClients:\n                    return new DashboardStats()\n                    {\n                        TopClients = totalStatCounter.GetTopClientStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopDomains:\n                    return new DashboardStats()\n                    {\n                        TopDomains = totalStatCounter.GetTopDomainStats(limit),\n                    };\n\n                case DashboardTopStatsType.TopBlockedDomains:\n                    return new DashboardStats()\n                    {\n                        TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit)\n                    };\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public List<KeyValuePair<DnsQuestionRecord, long>> GetLastHourEligibleQueries(int minimumHitsPerHour)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(-60);\n            lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc);\n\n            for (int minute = 0; minute < 60; minute++)\n            {\n                DateTime lastDateTime = lastHourDateTime.AddMinutes(minute);\n\n                StatCounter statCounter = _lastHourStatCountersCopy[lastDateTime.Minute];\n                if ((statCounter != null) && statCounter.IsLocked)\n                    totalStatCounter.Merge(statCounter);\n            }\n\n            return totalStatCounter.GetEligibleQueries(minimumHitsPerHour);\n        }\n\n        public Dictionary<NetworkAddress, ValueTuple<long, long>> GetLatestClientSubnetStats(int minutes, IEnumerable<int> ipv4Prefixes, IEnumerable<int> ipv6Prefixes)\n        {\n            StatCounter totalStatCounter = new StatCounter();\n            totalStatCounter.Lock();\n\n            DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(1 - minutes);\n            lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc);\n\n            for (int minute = 0; minute < minutes; minute++)\n            {\n                DateTime lastDateTime = lastHourDateTime.AddMinutes(minute);\n\n                StatCounter statCounter = _lastHourStatCounters[lastDateTime.Minute];\n                if (statCounter is not null)\n                    totalStatCounter.Merge(statCounter, false, true);\n            }\n\n            return totalStatCounter.GetClientSubnetStats(ipv4Prefixes, ipv6Prefixes);\n        }\n\n        #endregion\n\n        #region properties\n\n        public bool EnableInMemoryStats\n        {\n            get { return _enableInMemoryStats; }\n            set\n            {\n                if (_enableInMemoryStats != value)\n                {\n                    _enableInMemoryStats = value;\n\n                    if (_enableInMemoryStats)\n                    {\n                        _hourlyStatsCache.Clear();\n                        _dailyStatsCache.Clear();\n                    }\n                }\n            }\n        }\n\n        public int MaxStatFileDays\n        {\n            get { return _maxStatFileDays; }\n            set\n            {\n                if (value < 0)\n                    throw new ArgumentOutOfRangeException(nameof(MaxStatFileDays), \"MaxStatFileDays must be greater than or equal to 0.\");\n\n                _maxStatFileDays = value;\n\n                if (_maxStatFileDays == 0)\n                    _statsCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                else\n                    _statsCleanupTimer.Change(STATS_CLEANUP_TIMER_INITIAL_INTERVAL, STATS_CLEANUP_TIMER_PERIODIC_INTERVAL);\n            }\n        }\n\n        #endregion\n\n        class HourlyStats\n        {\n            #region variables\n\n            readonly StatCounter _hourStat; //calculated value\n            StatCounter[] _minuteStats = new StatCounter[60];\n\n            #endregion\n\n            #region constructor\n\n            public HourlyStats()\n            {\n                _hourStat = new StatCounter();\n                _hourStat.Lock();\n\n                for (int i = 0; i < _minuteStats.Length; i++)\n                {\n                    _minuteStats[i] = new StatCounter();\n                    _minuteStats[i].Lock();\n                }\n            }\n\n            public HourlyStats(BinaryReader bR)\n            {\n                if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"HS\") //format\n                    throw new InvalidDataException(\"HourlyStats format is invalid.\");\n\n                byte version = bR.ReadByte();\n                switch (version)\n                {\n                    case 1:\n                        _hourStat = new StatCounter();\n                        _hourStat.Lock();\n\n                        for (int i = 0; i < _minuteStats.Length; i++)\n                        {\n                            _minuteStats[i] = new StatCounter(bR);\n                            _hourStat.Merge(_minuteStats[i]);\n                        }\n\n                        break;\n\n                    default:\n                        throw new InvalidDataException(\"HourlyStats version not supported.\");\n                }\n            }\n\n            #endregion\n\n            #region public\n\n            public void UpdateStat(DateTime dateTime, StatCounter minuteStat)\n            {\n                if (!minuteStat.IsLocked)\n                    throw new DnsServerException(\"StatCounter must be locked.\");\n\n                _hourStat.Merge(minuteStat);\n                _minuteStats[dateTime.Minute] = minuteStat;\n            }\n\n            public void UnloadMinuteStats()\n            {\n                _minuteStats = null;\n            }\n\n            public void WriteTo(BinaryWriter bW)\n            {\n                bW.Write(Encoding.ASCII.GetBytes(\"HS\")); //format\n                bW.Write((byte)1); //version\n\n                for (int i = 0; i < _minuteStats.Length; i++)\n                {\n                    if (_minuteStats[i] == null)\n                    {\n                        _minuteStats[i] = new StatCounter();\n                        _minuteStats[i].Lock();\n                    }\n\n                    _minuteStats[i].WriteTo(bW);\n                }\n            }\n\n            #endregion\n\n            #region properties\n\n            public StatCounter HourStat\n            { get { return _hourStat; } }\n\n            public StatCounter[] MinuteStats\n            { get { return _minuteStats; } }\n\n            #endregion\n        }\n\n        class StatCounter\n        {\n            #region variables\n\n            volatile bool _locked;\n\n            long _totalQueries;\n            long _totalNoError;\n            long _totalServerFailure;\n            long _totalNxDomain;\n            long _totalRefused;\n\n            long _totalAuthoritative;\n            long _totalRecursive;\n            long _totalCached;\n            long _totalBlocked;\n            long _totalDropped;\n\n            long _totalClients;\n\n            readonly ConcurrentDictionary<string, Counter> _queryDomains;\n            readonly ConcurrentDictionary<string, Counter> _queryBlockedDomains;\n            readonly ConcurrentDictionary<DnsResourceRecordType, Counter> _queryTypes;\n            readonly ConcurrentDictionary<DnsTransportProtocol, Counter> _protocolTypes;\n            readonly ConcurrentDictionary<IPAddress, (Counter, Counter)> _clientIpAddressesUdpTcp;\n            readonly ConcurrentDictionary<DnsQuestionRecord, Counter> _queries;\n\n            bool _truncationFoundDuringMerge;\n            long _totalClientsDailyStatsSummation;\n\n            #endregion\n\n            #region constructor\n\n            public StatCounter()\n            {\n                _queryDomains = new ConcurrentDictionary<string, Counter>();\n                _queryBlockedDomains = new ConcurrentDictionary<string, Counter>();\n                _queryTypes = new ConcurrentDictionary<DnsResourceRecordType, Counter>();\n                _protocolTypes = new ConcurrentDictionary<DnsTransportProtocol, Counter>();\n                _clientIpAddressesUdpTcp = new ConcurrentDictionary<IPAddress, (Counter, Counter)>();\n                _queries = new ConcurrentDictionary<DnsQuestionRecord, Counter>();\n            }\n\n            public StatCounter(BinaryReader bR)\n            {\n                if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"SC\") //format\n                    throw new InvalidDataException(\"StatCounter format is invalid.\");\n\n                byte version = bR.ReadByte();\n                switch (version)\n                {\n                    case 1:\n                    case 2:\n                    case 3:\n                    case 4:\n                    case 5:\n                    case 6:\n                        _totalQueries = bR.ReadInt32();\n                        _totalNoError = bR.ReadInt32();\n                        _totalServerFailure = bR.ReadInt32();\n                        _totalNxDomain = bR.ReadInt32();\n                        _totalRefused = bR.ReadInt32();\n\n                        if (version >= 3)\n                        {\n                            _totalAuthoritative = bR.ReadInt32();\n                            _totalRecursive = bR.ReadInt32();\n                            _totalCached = bR.ReadInt32();\n                            _totalBlocked = bR.ReadInt32();\n                        }\n                        else\n                        {\n                            _totalBlocked = bR.ReadInt32();\n\n                            if (version >= 2)\n                                _totalCached = bR.ReadInt32();\n                        }\n\n                        if (version >= 6)\n                            _totalClients = bR.ReadInt32();\n\n                        {\n                            int count = bR.ReadInt32();\n                            _queryDomains = new ConcurrentDictionary<string, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queryDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt32()));\n                        }\n\n                        {\n                            int count = bR.ReadInt32();\n                            _queryBlockedDomains = new ConcurrentDictionary<string, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queryBlockedDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt32()));\n                        }\n\n                        {\n                            int count = bR.ReadInt32();\n                            _queryTypes = new ConcurrentDictionary<DnsResourceRecordType, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queryTypes.TryAdd((DnsResourceRecordType)bR.ReadUInt16(), new Counter(bR.ReadInt32()));\n                        }\n\n                        _protocolTypes = new ConcurrentDictionary<DnsTransportProtocol, Counter>(1, 0);\n\n                        {\n                            int count = bR.ReadInt32();\n                            _clientIpAddressesUdpTcp = new ConcurrentDictionary<IPAddress, (Counter, Counter)>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _clientIpAddressesUdpTcp.TryAdd(IPAddressExtensions.ReadFrom(bR), (new Counter(bR.ReadInt32()), new Counter()));\n\n                            if (version < 6)\n                                _totalClients = count;\n                        }\n\n                        if (version >= 4)\n                        {\n                            int count = bR.ReadInt32();\n                            _queries = new ConcurrentDictionary<DnsQuestionRecord, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queries.TryAdd(new DnsQuestionRecord(bR.BaseStream), new Counter(bR.ReadInt32()));\n                        }\n                        else\n                        {\n                            _queries = new ConcurrentDictionary<DnsQuestionRecord, Counter>(1, 0);\n                        }\n\n                        if (version >= 5)\n                        {\n                            int count = bR.ReadInt32();\n                            ConcurrentDictionary<IPAddress, Counter> errorIpAddresses = new ConcurrentDictionary<IPAddress, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                errorIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt32()));\n                        }\n\n                        break;\n\n                    case 7:\n                    case 8:\n                    case 9:\n                        _totalQueries = bR.ReadInt64();\n                        _totalNoError = bR.ReadInt64();\n                        _totalServerFailure = bR.ReadInt64();\n                        _totalNxDomain = bR.ReadInt64();\n                        _totalRefused = bR.ReadInt64();\n\n                        _totalAuthoritative = bR.ReadInt64();\n                        _totalRecursive = bR.ReadInt64();\n                        _totalCached = bR.ReadInt64();\n                        _totalBlocked = bR.ReadInt64();\n\n                        if (version >= 8)\n                            _totalDropped = bR.ReadInt64();\n\n                        _totalClients = bR.ReadInt64();\n\n                        {\n                            int count = bR.ReadInt32();\n                            _queryDomains = new ConcurrentDictionary<string, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queryDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt64()));\n                        }\n\n                        {\n                            int count = bR.ReadInt32();\n                            _queryBlockedDomains = new ConcurrentDictionary<string, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queryBlockedDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt64()));\n                        }\n\n                        {\n                            int count = bR.ReadInt32();\n                            _queryTypes = new ConcurrentDictionary<DnsResourceRecordType, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queryTypes.TryAdd((DnsResourceRecordType)bR.ReadUInt16(), new Counter(bR.ReadInt64()));\n                        }\n\n                        if (version >= 8)\n                        {\n                            int count = bR.ReadInt32();\n                            _protocolTypes = new ConcurrentDictionary<DnsTransportProtocol, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _protocolTypes.TryAdd((DnsTransportProtocol)bR.ReadByte(), new Counter(bR.ReadInt64()));\n                        }\n                        else\n                        {\n                            _protocolTypes = new ConcurrentDictionary<DnsTransportProtocol, Counter>(1, 0);\n                        }\n\n                        if (version >= 9)\n                        {\n                            int count = bR.ReadInt32();\n                            _clientIpAddressesUdpTcp = new ConcurrentDictionary<IPAddress, (Counter, Counter)>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _clientIpAddressesUdpTcp.TryAdd(IPAddressExtensions.ReadFrom(bR), (new Counter(bR.ReadInt64()), new Counter(bR.ReadInt64())));\n                        }\n                        else\n                        {\n                            int count = bR.ReadInt32();\n                            _clientIpAddressesUdpTcp = new ConcurrentDictionary<IPAddress, (Counter, Counter)>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _clientIpAddressesUdpTcp.TryAdd(IPAddressExtensions.ReadFrom(bR), (new Counter(bR.ReadInt64()), new Counter()));\n                        }\n\n                        {\n                            int count = bR.ReadInt32();\n                            _queries = new ConcurrentDictionary<DnsQuestionRecord, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                _queries.TryAdd(new DnsQuestionRecord(bR.BaseStream), new Counter(bR.ReadInt64()));\n                        }\n\n                        if (version <= 8)\n                        {\n                            int count = bR.ReadInt32();\n                            ConcurrentDictionary<IPAddress, Counter> errorIpAddresses = new ConcurrentDictionary<IPAddress, Counter>(1, count);\n\n                            for (int i = 0; i < count; i++)\n                                errorIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt64()));\n                        }\n\n                        break;\n\n                    default:\n                        throw new InvalidDataException(\"StatCounter version not supported.\");\n                }\n\n                _locked = true;\n            }\n\n            #endregion\n\n            #region private\n\n            private static List<KeyValuePair<string, T>> GetTopList<T>(List<KeyValuePair<string, T>> list, int limit) where T : DashboardStats.TopStats\n            {\n                list.Sort(delegate (KeyValuePair<string, T> item1, KeyValuePair<string, T> item2)\n                {\n                    return item2.Value.Hits.CompareTo(item1.Value.Hits);\n                });\n\n                if (list.Count > limit)\n                    list.RemoveRange(limit, list.Count - limit);\n\n                return list;\n            }\n\n            private static Counter GetNewCounter<T>(T key)\n            {\n                return new Counter();\n            }\n\n            private static (Counter, Counter) GetNewCounterTuple<T>(T key)\n            {\n                return (new Counter(), new Counter());\n            }\n\n            #endregion\n\n            #region public\n\n            public void Lock()\n            {\n                _locked = true;\n            }\n\n            public void Update(DnsQuestionRecord query, DnsResponseCode responseCode, DnsServerResponseType responseType, IPAddress clientIpAddress, DnsTransportProtocol protocol, bool rateLimited)\n            {\n                if (_locked)\n                    return;\n\n                if (clientIpAddress.IsIPv4MappedToIPv6)\n                    clientIpAddress = clientIpAddress.MapToIPv4();\n\n                _totalQueries++;\n\n                if (responseType == DnsServerResponseType.Dropped)\n                {\n                    _totalDropped++;\n\n                    if (rateLimited)\n                    {\n                        if (protocol == DnsTransportProtocol.Udp)\n                            _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item1.Increment();\n                        else\n                            _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item2.Increment();\n\n                        _totalClients = _clientIpAddressesUdpTcp.Count;\n                    }\n                }\n                else\n                {\n                    switch (responseCode)\n                    {\n                        case DnsResponseCode.NoError:\n                            if (query is not null)\n                            {\n                                switch (responseType)\n                                {\n                                    case DnsServerResponseType.Blocked:\n                                    case DnsServerResponseType.UpstreamBlocked:\n                                    case DnsServerResponseType.UpstreamBlockedCached:\n                                        //skip blocked domains\n                                        break;\n\n                                    default:\n                                        _queryDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment();\n                                        _queries.GetOrAdd(query, GetNewCounter).Increment();\n                                        break;\n                                }\n                            }\n\n                            _totalNoError++;\n                            break;\n\n                        case DnsResponseCode.ServerFailure:\n                            _totalServerFailure++;\n                            break;\n\n                        case DnsResponseCode.NxDomain:\n                            _totalNxDomain++;\n                            break;\n\n                        case DnsResponseCode.Refused:\n                            _totalRefused++;\n                            break;\n\n                        case DnsResponseCode.FormatError:\n                            break;\n                    }\n\n                    switch (responseType)\n                    {\n                        case DnsServerResponseType.Authoritative:\n                            _totalAuthoritative++;\n                            break;\n\n                        case DnsServerResponseType.Recursive:\n                            _totalRecursive++;\n                            break;\n\n                        case DnsServerResponseType.Cached:\n                            _totalCached++;\n                            break;\n\n                        case DnsServerResponseType.Blocked:\n                            if (query is not null)\n                                _queryBlockedDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment();\n\n                            _totalBlocked++;\n                            break;\n\n                        case DnsServerResponseType.UpstreamBlocked:\n                            _totalRecursive++;\n\n                            if (query is not null)\n                                _queryBlockedDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment();\n\n                            _totalBlocked++;\n                            break;\n\n                        case DnsServerResponseType.UpstreamBlockedCached:\n                            _totalCached++;\n\n                            if (query is not null)\n                                _queryBlockedDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment();\n\n                            _totalBlocked++;\n                            break;\n                    }\n\n                    if (query is not null)\n                        _queryTypes.GetOrAdd(query.Type, GetNewCounter).Increment();\n\n                    if (protocol == DnsTransportProtocol.Udp)\n                        _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item1.Increment();\n                    else\n                        _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item2.Increment();\n\n                    _totalClients = _clientIpAddressesUdpTcp.Count;\n                }\n\n                _protocolTypes.GetOrAdd(protocol, GetNewCounter).Increment();\n            }\n\n            public void Merge(StatCounter statCounter, bool isDailyStatCounter = false, bool skipLock = false)\n            {\n                if (!skipLock && (!_locked || !statCounter._locked))\n                    throw new DnsServerException(\"StatCounter must be locked.\");\n\n                _totalQueries += statCounter._totalQueries;\n                _totalNoError += statCounter._totalNoError;\n                _totalServerFailure += statCounter._totalServerFailure;\n                _totalNxDomain += statCounter._totalNxDomain;\n                _totalRefused += statCounter._totalRefused;\n\n                _totalAuthoritative += statCounter._totalAuthoritative;\n                _totalRecursive += statCounter._totalRecursive;\n                _totalCached += statCounter._totalCached;\n                _totalBlocked += statCounter._totalBlocked;\n                _totalDropped += statCounter._totalDropped;\n\n                foreach (KeyValuePair<string, Counter> queryDomain in statCounter._queryDomains)\n                    _queryDomains.GetOrAdd(queryDomain.Key, GetNewCounter).Merge(queryDomain.Value);\n\n                foreach (KeyValuePair<string, Counter> queryBlockedDomain in statCounter._queryBlockedDomains)\n                    _queryBlockedDomains.GetOrAdd(queryBlockedDomain.Key, GetNewCounter).Merge(queryBlockedDomain.Value);\n\n                foreach (KeyValuePair<DnsResourceRecordType, Counter> queryType in statCounter._queryTypes)\n                    _queryTypes.GetOrAdd(queryType.Key, GetNewCounter).Merge(queryType.Value);\n\n                foreach (KeyValuePair<DnsTransportProtocol, Counter> protocolType in statCounter._protocolTypes)\n                    _protocolTypes.GetOrAdd(protocolType.Key, GetNewCounter).Merge(protocolType.Value);\n\n                foreach (KeyValuePair<IPAddress, (Counter, Counter)> clientIpAddress in statCounter._clientIpAddressesUdpTcp)\n                {\n                    (Counter, Counter) counterTuple = _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress.Key, GetNewCounterTuple);\n                    counterTuple.Item1.Merge(clientIpAddress.Value.Item1);\n                    counterTuple.Item2.Merge(clientIpAddress.Value.Item2);\n                }\n\n                foreach (KeyValuePair<DnsQuestionRecord, Counter> query in statCounter._queries)\n                    _queries.GetOrAdd(query.Key, GetNewCounter).Merge(query.Value);\n\n                _totalClients = _clientIpAddressesUdpTcp.Count;\n                _totalClientsDailyStatsSummation += statCounter._totalClients;\n\n                if (isDailyStatCounter && (statCounter._totalClients > statCounter._clientIpAddressesUdpTcp.Count))\n                    _truncationFoundDuringMerge = true;\n            }\n\n            public bool Truncate(int limit)\n            {\n                bool truncated = false;\n\n                if (_queryDomains.Count > limit)\n                {\n                    List<KeyValuePair<string, Counter>> topDomains = new List<KeyValuePair<string, Counter>>(_queryDomains);\n\n                    _queryDomains.Clear();\n\n                    topDomains.Sort(delegate (KeyValuePair<string, Counter> item1, KeyValuePair<string, Counter> item2)\n                    {\n                        return item2.Value.Count.CompareTo(item1.Value.Count);\n                    });\n\n                    if (topDomains.Count > limit)\n                        topDomains.RemoveRange(limit, topDomains.Count - limit);\n\n                    foreach (KeyValuePair<string, Counter> item in topDomains)\n                        _queryDomains[item.Key] = item.Value;\n\n                    truncated = true;\n                }\n\n                if (_queryBlockedDomains.Count > limit)\n                {\n                    List<KeyValuePair<string, Counter>> topBlockedDomains = new List<KeyValuePair<string, Counter>>(_queryBlockedDomains);\n\n                    _queryBlockedDomains.Clear();\n\n                    topBlockedDomains.Sort(delegate (KeyValuePair<string, Counter> item1, KeyValuePair<string, Counter> item2)\n                    {\n                        return item2.Value.Count.CompareTo(item1.Value.Count);\n                    });\n\n                    if (topBlockedDomains.Count > limit)\n                        topBlockedDomains.RemoveRange(limit, topBlockedDomains.Count - limit);\n\n                    foreach (KeyValuePair<string, Counter> item in topBlockedDomains)\n                        _queryBlockedDomains[item.Key] = item.Value;\n\n                    truncated = true;\n                }\n\n                if (_queryTypes.Count > limit)\n                {\n                    List<KeyValuePair<DnsResourceRecordType, Counter>> queryTypes = new List<KeyValuePair<DnsResourceRecordType, Counter>>(_queryTypes);\n\n                    _queryTypes.Clear();\n\n                    queryTypes.Sort(delegate (KeyValuePair<DnsResourceRecordType, Counter> item1, KeyValuePair<DnsResourceRecordType, Counter> item2)\n                    {\n                        return item2.Value.Count.CompareTo(item1.Value.Count);\n                    });\n\n                    if (queryTypes.Count > limit)\n                    {\n                        long othersCount = 0;\n\n                        for (int i = limit; i < queryTypes.Count; i++)\n                            othersCount += queryTypes[i].Value.Count;\n\n                        queryTypes.RemoveRange(limit - 1, queryTypes.Count - (limit - 1));\n                        queryTypes.Add(new KeyValuePair<DnsResourceRecordType, Counter>(DnsResourceRecordType.Unknown, new Counter(othersCount)));\n                    }\n\n                    foreach (KeyValuePair<DnsResourceRecordType, Counter> item in queryTypes)\n                        _queryTypes[item.Key] = item.Value;\n\n                    truncated = true;\n                }\n\n                if (_clientIpAddressesUdpTcp.Count > limit)\n                {\n                    List<KeyValuePair<IPAddress, (Counter, Counter)>> topClients = new List<KeyValuePair<IPAddress, (Counter, Counter)>>(_clientIpAddressesUdpTcp);\n\n                    _clientIpAddressesUdpTcp.Clear();\n\n                    topClients.Sort(delegate (KeyValuePair<IPAddress, (Counter, Counter)> x, KeyValuePair<IPAddress, (Counter, Counter)> y)\n                    {\n                        long x1 = x.Value.Item1.Count + x.Value.Item2.Count;\n                        long y1 = y.Value.Item1.Count + y.Value.Item2.Count;\n\n                        return y1.CompareTo(x1);\n                    });\n\n                    if (topClients.Count > limit)\n                        topClients.RemoveRange(limit, topClients.Count - limit);\n\n                    foreach (KeyValuePair<IPAddress, (Counter, Counter)> item in topClients)\n                        _clientIpAddressesUdpTcp[item.Key] = item.Value;\n\n                    truncated = true;\n                }\n\n                if (_queries.Count > limit)\n                {\n                    //only last hour queries data is required for cache auto prefetching\n                    _queries.Clear();\n\n                    truncated = true;\n                }\n\n                return truncated;\n            }\n\n            public void WriteTo(BinaryWriter bW)\n            {\n                if (!_locked)\n                    throw new DnsServerException(\"StatCounter must be locked.\");\n\n                bW.Write(Encoding.ASCII.GetBytes(\"SC\")); //format\n                bW.Write((byte)9); //version\n\n                bW.Write(_totalQueries);\n                bW.Write(_totalNoError);\n                bW.Write(_totalServerFailure);\n                bW.Write(_totalNxDomain);\n                bW.Write(_totalRefused);\n\n                bW.Write(_totalAuthoritative);\n                bW.Write(_totalRecursive);\n                bW.Write(_totalCached);\n                bW.Write(_totalBlocked);\n                bW.Write(_totalDropped);\n\n                bW.Write(_totalClients);\n\n                {\n                    bW.Write(_queryDomains.Count);\n                    foreach (KeyValuePair<string, Counter> queryDomain in _queryDomains)\n                    {\n                        bW.WriteShortString(queryDomain.Key);\n                        bW.Write(queryDomain.Value.Count);\n                    }\n                }\n\n                {\n                    bW.Write(_queryBlockedDomains.Count);\n                    foreach (KeyValuePair<string, Counter> queryBlockedDomain in _queryBlockedDomains)\n                    {\n                        bW.WriteShortString(queryBlockedDomain.Key);\n                        bW.Write(queryBlockedDomain.Value.Count);\n                    }\n                }\n\n                {\n                    bW.Write(_queryTypes.Count);\n                    foreach (KeyValuePair<DnsResourceRecordType, Counter> queryType in _queryTypes)\n                    {\n                        bW.Write((ushort)queryType.Key);\n                        bW.Write(queryType.Value.Count);\n                    }\n                }\n\n                {\n                    bW.Write(_protocolTypes.Count);\n                    foreach (KeyValuePair<DnsTransportProtocol, Counter> protocolType in _protocolTypes)\n                    {\n                        bW.Write((byte)protocolType.Key);\n                        bW.Write(protocolType.Value.Count);\n                    }\n                }\n\n                {\n                    bW.Write(_clientIpAddressesUdpTcp.Count);\n                    foreach (KeyValuePair<IPAddress, (Counter, Counter)> clientIpAddress in _clientIpAddressesUdpTcp)\n                    {\n                        clientIpAddress.Key.WriteTo(bW);\n                        bW.Write(clientIpAddress.Value.Item1.Count);\n                        bW.Write(clientIpAddress.Value.Item2.Count);\n                    }\n                }\n\n                {\n                    bW.Write(_queries.Count);\n                    foreach (KeyValuePair<DnsQuestionRecord, Counter> query in _queries)\n                    {\n                        query.Key.WriteTo(bW.BaseStream, null);\n                        bW.Write(query.Value.Count);\n                    }\n                }\n            }\n\n            public DashboardStats.StatsData GetStatsData()\n            {\n                return new DashboardStats.StatsData\n                {\n                    TotalQueries = _totalQueries,\n                    TotalNoError = _totalNoError,\n                    TotalServerFailure = _totalServerFailure,\n                    TotalNxDomain = _totalNxDomain,\n                    TotalRefused = _totalRefused,\n\n                    TotalAuthoritative = _totalAuthoritative,\n                    TotalRecursive = _totalRecursive,\n                    TotalCached = _totalCached,\n                    TotalBlocked = _totalBlocked,\n                    TotalDropped = _totalDropped,\n\n                    TotalClients = _totalClients\n                };\n            }\n\n            public DashboardStats.ChartData GetQueryResponseChartData()\n            {\n                return new DashboardStats.ChartData()\n                {\n                    Labels =\n                    [\n                        \"Authoritative\",\n                        \"Recursive\",\n                        \"Cached\",\n                        \"Blocked\",\n                        \"Dropped\"\n                    ],\n                    DataSets =\n                    [\n                        new DashboardStats.DataSet()\n                        {\n                            Data =\n                            [\n                                _totalAuthoritative,\n                                _totalRecursive,\n                                _totalCached,\n                                _totalBlocked,\n                                _totalDropped\n                            ]\n                        }\n                    ]\n                };\n            }\n\n            public DashboardStats.TopStats[] GetTopDomainStats(int limit)\n            {\n                List<KeyValuePair<string, DashboardStats.TopStats>> topDomainsList = new List<KeyValuePair<string, DashboardStats.TopStats>>(_queryDomains.Count);\n\n                foreach (KeyValuePair<string, Counter> item in _queryDomains)\n                    topDomainsList.Add(new KeyValuePair<string, DashboardStats.TopStats>(item.Key, new DashboardStats.TopStats { Name = item.Key, Hits = item.Value.Count }));\n\n                List<KeyValuePair<string, DashboardStats.TopStats>> topDomainsData = GetTopList(topDomainsList, limit);\n                DashboardStats.TopStats[] topDomains = new DashboardStats.TopStats[topDomainsData.Count];\n\n                for (int i = 0; i < topDomainsData.Count; i++)\n                    topDomains[i] = topDomainsData[i].Value;\n\n                return topDomains;\n            }\n\n            public DashboardStats.TopStats[] GetTopBlockedDomainStats(int limit)\n            {\n                List<KeyValuePair<string, DashboardStats.TopStats>> topBlockedDomainsList = new List<KeyValuePair<string, DashboardStats.TopStats>>(_queryBlockedDomains.Count);\n\n                foreach (KeyValuePair<string, Counter> item in _queryBlockedDomains)\n                    topBlockedDomainsList.Add(new KeyValuePair<string, DashboardStats.TopStats>(item.Key, new DashboardStats.TopStats { Name = item.Key, Hits = item.Value.Count }));\n\n                List<KeyValuePair<string, DashboardStats.TopStats>> topBlockedDomainsData = GetTopList(topBlockedDomainsList, limit);\n                DashboardStats.TopStats[] topBlockedDomains = new DashboardStats.TopStats[topBlockedDomainsData.Count];\n\n                for (int i = 0; i < topBlockedDomainsData.Count; i++)\n                    topBlockedDomains[i] = topBlockedDomainsData[i].Value;\n\n                return topBlockedDomains;\n            }\n\n            public DashboardStats.TopClientStats[] GetTopClientStats(int limit)\n            {\n                List<KeyValuePair<string, DashboardStats.TopClientStats>> topClientsList = new List<KeyValuePair<string, DashboardStats.TopClientStats>>(_clientIpAddressesUdpTcp.Count);\n\n                foreach (KeyValuePair<IPAddress, (Counter, Counter)> item in _clientIpAddressesUdpTcp)\n                    topClientsList.Add(new KeyValuePair<string, DashboardStats.TopClientStats>(item.Key.ToString(), new DashboardStats.TopClientStats { Name = item.Key.ToString(), Hits = item.Value.Item1.Count + item.Value.Item2.Count }));\n\n                List<KeyValuePair<string, DashboardStats.TopClientStats>> topClientsData = GetTopList(topClientsList, limit);\n                DashboardStats.TopClientStats[] topClients = new DashboardStats.TopClientStats[topClientsData.Count];\n\n                for (int i = 0; i < topClientsData.Count; i++)\n                    topClients[i] = topClientsData[i].Value;\n\n                return topClients;\n            }\n\n            public DashboardStats.ChartData GetTopQueryTypesChartData()\n            {\n                List<KeyValuePair<string, long>> queryTypes = new List<KeyValuePair<string, long>>(_queryTypes.Count);\n\n                foreach (KeyValuePair<DnsResourceRecordType, Counter> item in _queryTypes)\n                    queryTypes.Add(new KeyValuePair<string, long>(item.Key.ToString(), item.Value.Count));\n\n                queryTypes.Sort(delegate (KeyValuePair<string, long> item1, KeyValuePair<string, long> item2)\n                {\n                    return item2.Value.CompareTo(item1.Value);\n                });\n\n                string[] queryTypeLabels = new string[queryTypes.Count];\n                long[] queryTypeData = new long[queryTypes.Count];\n\n                for (int i = 0; i < queryTypes.Count; i++)\n                {\n                    KeyValuePair<string, long> topQueryTypeData = queryTypes[i];\n\n                    queryTypeLabels[i] = topQueryTypeData.Key;\n                    queryTypeData[i] = topQueryTypeData.Value;\n                }\n\n                return new DashboardStats.ChartData()\n                {\n                    Labels = queryTypeLabels,\n                    DataSets =\n                    [\n                        new DashboardStats.DataSet()\n                        {\n                            Data = queryTypeData\n                        }\n                    ]\n                };\n            }\n\n            public DashboardStats.ChartData GetTopProtocolTypesChartData()\n            {\n                List<KeyValuePair<string, long>> protocolTypes = new List<KeyValuePair<string, long>>(_protocolTypes.Count);\n\n                foreach (KeyValuePair<DnsTransportProtocol, Counter> protocolType in _protocolTypes)\n                    protocolTypes.Add(new KeyValuePair<string, long>(protocolType.Key.ToString(), protocolType.Value.Count));\n\n                protocolTypes.Sort(delegate (KeyValuePair<string, long> item1, KeyValuePair<string, long> item2)\n                {\n                    return item2.Value.CompareTo(item1.Value);\n                });\n\n                string[] topProtocolLabels = new string[protocolTypes.Count];\n                long[] topProtocolData = new long[protocolTypes.Count];\n\n                for (int i = 0; i < protocolTypes.Count; i++)\n                {\n                    KeyValuePair<string, long> topProtocolTypeData = protocolTypes[i];\n\n                    topProtocolLabels[i] = topProtocolTypeData.Key;\n                    topProtocolData[i] = topProtocolTypeData.Value;\n                }\n\n                return new DashboardStats.ChartData()\n                {\n                    Labels = topProtocolLabels,\n                    DataSets =\n                    [\n                        new DashboardStats.DataSet()\n                        {\n                            Data = topProtocolData\n                        }\n                    ]\n                };\n            }\n\n            public List<KeyValuePair<DnsQuestionRecord, long>> GetEligibleQueries(int minimumHits)\n            {\n                List<KeyValuePair<DnsQuestionRecord, long>> eligibleQueries = new List<KeyValuePair<DnsQuestionRecord, long>>(Convert.ToInt32(_queries.Count * 0.1));\n\n                foreach (KeyValuePair<DnsQuestionRecord, Counter> item in _queries)\n                {\n                    if (item.Value.Count >= minimumHits)\n                        eligibleQueries.Add(new KeyValuePair<DnsQuestionRecord, long>(item.Key, item.Value.Count));\n                }\n\n                return eligibleQueries;\n            }\n\n            public Dictionary<NetworkAddress, (long, long)> GetClientSubnetStats(IEnumerable<int> ipv4Prefixes, IEnumerable<int> ipv6Prefixes)\n            {\n                Dictionary<NetworkAddress, (long, long)> clientSubnetStats = new Dictionary<NetworkAddress, (long, long)>(_clientIpAddressesUdpTcp.Count);\n\n                void UpdateClientSubnetStats(NetworkAddress clientSubnet, (Counter, Counter) value)\n                {\n                    if (clientSubnetStats.TryGetValue(clientSubnet, out ValueTuple<long, long> existingValue))\n                    {\n                        existingValue.Item1 += value.Item1.Count;\n                        existingValue.Item2 += value.Item2.Count;\n                    }\n                    else\n                    {\n                        clientSubnetStats.Add(clientSubnet, (value.Item1.Count, value.Item2.Count));\n                    }\n                }\n\n                foreach (KeyValuePair<IPAddress, (Counter, Counter)> item in _clientIpAddressesUdpTcp)\n                {\n                    switch (item.Key.AddressFamily)\n                    {\n                        case AddressFamily.InterNetwork:\n                            IPAddress clientIPv4 = item.Key;\n\n                            foreach (int ipv4Prefix in ipv4Prefixes)\n                                UpdateClientSubnetStats(new NetworkAddress(clientIPv4, (byte)ipv4Prefix), item.Value);\n\n                            break;\n\n                        case AddressFamily.InterNetworkV6:\n                            IPAddress clientIPv6 = item.Key;\n\n                            foreach (int ipv6Prefix in ipv6Prefixes)\n                                UpdateClientSubnetStats(new NetworkAddress(clientIPv6, (byte)ipv6Prefix), item.Value);\n\n                            break;\n\n                        default:\n                            throw new NotSupportedException(\"AddressFamily not supported.\");\n                    }\n                }\n\n                return clientSubnetStats;\n            }\n\n            #endregion\n\n            #region properties\n\n            public bool IsLocked\n            { get { return _locked; } }\n\n            public long TotalQueries\n            { get { return _totalQueries; } }\n\n            public long TotalNoError\n            { get { return _totalNoError; } }\n\n            public long TotalServerFailure\n            { get { return _totalServerFailure; } }\n\n            public long TotalNxDomain\n            { get { return _totalNxDomain; } }\n\n            public long TotalRefused\n            { get { return _totalRefused; } }\n\n            public long TotalAuthoritative\n            { get { return _totalAuthoritative; } }\n\n            public long TotalRecursive\n            { get { return _totalRecursive; } }\n\n            public long TotalCached\n            { get { return _totalCached; } }\n\n            public long TotalBlocked\n            { get { return _totalBlocked; } }\n\n            public long TotalDropped\n            { get { return _totalDropped; } }\n\n            public long TotalClients\n            {\n                get\n                {\n                    if (_truncationFoundDuringMerge)\n                        return _totalClientsDailyStatsSummation;\n\n                    return _totalClients;\n                }\n            }\n\n            #endregion\n\n            class Counter\n            {\n                #region variables\n\n                long _count;\n\n                #endregion\n\n                #region constructor\n\n                public Counter()\n                { }\n\n                public Counter(long count)\n                {\n                    _count = count;\n                }\n\n                #endregion\n\n                #region public\n\n                public void Increment()\n                {\n                    _count++;\n                }\n\n                public void Merge(Counter counter)\n                {\n                    _count += counter._count;\n                }\n\n                #endregion\n\n                #region properties\n\n                public long Count\n                { get { return _count; } }\n\n                #endregion\n            }\n        }\n\n        readonly struct StatsQueueItem\n        {\n            #region variables\n\n            public readonly DateTime _timestamp;\n\n            public readonly DnsDatagram _request;\n            public readonly IPEndPoint _remoteEP;\n            public readonly DnsTransportProtocol _protocol;\n            public readonly DnsDatagram _response;\n            public readonly bool _rateLimited;\n\n            #endregion\n\n            #region constructor\n\n            public StatsQueueItem(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response, bool rateLimited)\n            {\n                _timestamp = DateTime.UtcNow;\n\n                _request = request;\n                _remoteEP = remoteEP;\n                _protocol = protocol;\n                _response = response;\n                _rateLimited = rateLimited;\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Trees/AuthZoneNode.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2022  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Trees\n{\n    class AuthZoneNode : IDisposable\n    {\n        #region variables\n\n        SubDomainZone _parentSideZone;\n        ApexZone _apexZone;\n\n        #endregion\n\n        #region constructors\n\n        public AuthZoneNode(SubDomainZone parentSideZone, ApexZone zone)\n        {\n            _parentSideZone = parentSideZone;\n            _apexZone = zone;\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            if (_apexZone is not null)\n                _apexZone.Dispose();\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region public\n\n        public bool TryAdd(ApexZone apexZone)\n        {\n            return Interlocked.CompareExchange(ref _apexZone, apexZone, null) is null;\n        }\n\n        public bool TryAdd(SubDomainZone parentSideZone)\n        {\n            return Interlocked.CompareExchange(ref _parentSideZone, parentSideZone, null) is null;\n        }\n\n        public bool TryRemove(out ApexZone apexZone)\n        {\n            apexZone = _apexZone;\n            return ReferenceEquals(Interlocked.CompareExchange(ref _apexZone, null, apexZone), apexZone);\n        }\n\n        public bool TryRemove(out SubDomainZone parentSideZone)\n        {\n            parentSideZone = _parentSideZone;\n            return ReferenceEquals(Interlocked.CompareExchange(ref _parentSideZone, null, parentSideZone), parentSideZone);\n        }\n\n        public SubDomainZone GetOrAddParentSideZone(Func<SubDomainZone> valueFactory)\n        {\n            SubDomainZone newParentSideZone = null;\n\n            while (true)\n            {\n                SubDomainZone parentSideZone = _parentSideZone;\n                if (parentSideZone is not null)\n                    return parentSideZone;\n\n                if (newParentSideZone is null)\n                    newParentSideZone = valueFactory();\n\n                if (TryAdd(newParentSideZone))\n                    return newParentSideZone;\n            }\n        }\n\n        public IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            if ((_apexZone is null) || (type == DnsResourceRecordType.DS))\n            {\n                if (_parentSideZone is null)\n                    return Array.Empty<DnsResourceRecord>();\n\n                return _parentSideZone.QueryRecords(type, dnssecOk);\n            }\n\n            return _apexZone.QueryRecords(type, dnssecOk);\n        }\n\n        public AuthZone GetAuthZone(string zoneName)\n        {\n            if ((_apexZone is not null) && _apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                return _apexZone;\n\n            return _parentSideZone;\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Name\n        {\n            get\n            {\n                if (_parentSideZone is not null)\n                    return _parentSideZone.Name;\n\n                if (_apexZone is not null)\n                    return _apexZone.Name;\n\n                return null;\n            }\n        }\n\n        public SubDomainZone ParentSideZone\n        { get { return _parentSideZone; } }\n\n        public ApexZone ApexZone\n        { get { return _apexZone; } }\n\n        public bool IsActive\n        {\n            get\n            {\n                if (_apexZone is not null)\n                    return _apexZone.IsActive;\n\n                if (_parentSideZone is not null)\n                    return _parentSideZone.IsActive;\n\n                return false;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Trees/AuthZoneTree.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ZoneManagers;\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Trees\n{\n    class AuthZoneTree : ZoneTree<AuthZoneNode, SubDomainZone, ApexZone>\n    {\n        #region variables\n\n        static readonly char[] _starPeriodTrimChars = new char[] { '*', '.' };\n\n        #endregion\n\n        #region private\n\n        private static Node GetPreviousSubDomainZoneNode(byte[] key, Node currentNode, int baseDepth)\n        {\n            int k;\n\n            NodeValue currentValue = currentNode.Value;\n            if (currentValue is null)\n            {\n                //key value does not exists\n                if (currentNode.Children is null)\n                {\n                    //no children available; move to previous sibling\n                    k = currentNode.K - 1; //find previous node from sibling starting at k - 1\n                    currentNode = currentNode.Parent;\n                }\n                else\n                {\n                    if (key.Length == currentNode.Depth)\n                    {\n                        //current node belongs to the key\n                        k = currentNode.K - 1; //find previous node from sibling starting at k - 1\n                        currentNode = currentNode.Parent;\n                    }\n                    else\n                    {\n                        //find the previous node for the given k in current node's children\n                        k = key[currentNode.Depth];\n                    }\n                }\n            }\n            else\n            {\n                int x = DnsNSECRecordData.CanonicalComparison(currentValue.Key, key);\n                if (x == 0)\n                {\n                    //current node value matches the key\n                    k = currentNode.K - 1; //find previous node from sibling starting at k - 1\n                    currentNode = currentNode.Parent;\n                }\n                else if (x > 0)\n                {\n                    //current node value is larger for the key\n                    k = currentNode.K - 1; //find previous node from sibling starting at k - 1\n                    currentNode = currentNode.Parent;\n                }\n                else\n                {\n                    //current node value is smaller for the key\n                    if (currentNode.Children is null)\n                    {\n                        //the current node is previous node since no children exists and value is smaller for the key\n                        return currentNode;\n                    }\n                    else\n                    {\n                        //find the previous node for the given k in current node's children\n                        k = key[currentNode.Depth];\n                    }\n                }\n            }\n\n            //start reverse tree traversal\n            while ((currentNode is not null) && (currentNode.Depth >= baseDepth))\n            {\n                Node[] children = currentNode.Children;\n                if (children is not null)\n                {\n                    //find previous child node\n                    Node child = null;\n\n                    for (int i = k; i > -1; i--)\n                    {\n                        child = Volatile.Read(ref children[i]);\n                        if (child is not null)\n                        {\n                            bool childNodeHasApexZone = false;\n\n                            NodeValue childValue = child.Value;\n                            if (childValue is not null)\n                            {\n                                AuthZoneNode authZoneNode = childValue.Value;\n                                if (authZoneNode is not null)\n                                {\n                                    if (authZoneNode.ApexZone is not null)\n                                        childNodeHasApexZone = true; //must stop checking children of the apex of the sub zone\n                                }\n                            }\n\n                            if (!childNodeHasApexZone && child.Children is not null)\n                                break; //child has further children so check them first\n\n                            if (childValue is not null)\n                            {\n                                AuthZoneNode authZoneNode = childValue.Value;\n                                if (authZoneNode is not null)\n                                {\n                                    if (authZoneNode.ParentSideZone is not null)\n                                    {\n                                        //is sub domain zone\n                                        return child; //child has value so return it\n                                    }\n\n                                    if (authZoneNode.ApexZone is not null)\n                                    {\n                                        //is apex zone\n                                        //skip to next child to avoid listing this auth zone's sub domains\n                                        child = null; //set null to avoid child being set as current after the loop\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    if (child is not null)\n                    {\n                        //make found child as current\n                        k = children.Length - 1;\n                        currentNode = child;\n                        continue; //start over\n                    }\n                }\n\n                //no child node available; check for current node value\n                {\n                    NodeValue value = currentNode.Value;\n                    if (value is not null)\n                    {\n                        AuthZoneNode authZoneNode = value.Value;\n                        if (authZoneNode is not null)\n                        {\n                            if ((authZoneNode.ApexZone is not null) && (currentNode.Depth == baseDepth))\n                            {\n                                //current node contains apex zone for the base depth i.e. current zone; return it\n                                return currentNode;\n                            }\n\n                            if (authZoneNode.ParentSideZone is not null)\n                            {\n                                //current node contains sub domain zone; return it\n                                return currentNode;\n                            }\n                        }\n                    }\n                }\n\n                //move up to parent node for previous sibling\n                k = currentNode.K - 1;\n                currentNode = currentNode.Parent;\n            }\n\n            return null;\n        }\n\n        private static Node GetNextSubDomainZoneNode(byte[] key, Node currentNode, int baseDepth)\n        {\n            int k;\n\n            NodeValue currentValue = currentNode.Value;\n            if (currentValue is null)\n            {\n                //key value does not exists\n                if (currentNode.Children is null)\n                {\n                    //no children available; move to next sibling\n                    k = currentNode.K + 1; //find next node from sibling starting at k + 1\n                    currentNode = currentNode.Parent;\n                }\n                else\n                {\n                    if (key.Length == currentNode.Depth)\n                    {\n                        //current node belongs to the key\n                        k = 0; //find next node from first child of current node\n                    }\n                    else\n                    {\n                        //find next node for the given k in current node's children\n                        k = key[currentNode.Depth];\n                    }\n                }\n            }\n            else\n            {\n                //check if node contains apex zone\n                bool foundApexZone = false;\n\n                if (currentNode.Depth > baseDepth)\n                {\n                    AuthZoneNode authZoneNode = currentValue.Value;\n                    if (authZoneNode is not null)\n                    {\n                        ApexZone apexZone = authZoneNode.ApexZone;\n                        if (apexZone is not null)\n                            foundApexZone = true;\n                    }\n                }\n\n                if (foundApexZone)\n                {\n                    //current contains apex for a sub zone; move up to parent node\n                    k = currentNode.K + 1; //find next node from sibling starting at k + 1\n                    currentNode = currentNode.Parent;\n                }\n                else\n                {\n                    int x = DnsNSECRecordData.CanonicalComparison(currentValue.Key, key);\n                    if (x == 0)\n                    {\n                        //current node value matches the key\n                        k = 0; //find next node from children starting at k\n                    }\n                    else if (x > 0)\n                    {\n                        //current node value is larger for the key thus current is the next node\n                        return currentNode;\n                    }\n                    else\n                    {\n                        //current node value is smaller for the key\n                        k = key[currentNode.Depth]; //find next node from children starting at k = key[depth]\n                    }\n                }\n            }\n\n            //start tree traversal\n            while ((currentNode is not null) && (currentNode.Depth >= baseDepth))\n            {\n                Node[] children = currentNode.Children;\n                if (children is not null)\n                {\n                    //find next child node\n                    Node child = null;\n\n                    for (int i = k; i < children.Length; i++)\n                    {\n                        child = Volatile.Read(ref children[i]);\n                        if (child is not null)\n                        {\n                            NodeValue childValue = child.Value;\n                            if (childValue is not null)\n                            {\n                                AuthZoneNode authZoneNode = childValue.Value;\n                                if (authZoneNode is not null)\n                                {\n                                    if (authZoneNode.ParentSideZone is not null)\n                                    {\n                                        //is sub domain zone\n                                        return child; //child has value so return it\n                                    }\n\n                                    if (authZoneNode.ApexZone is not null)\n                                    {\n                                        //is apex zone\n                                        //skip to next child to avoid listing this auth zone's sub domains\n                                        child = null; //set null to avoid child being set as current after the loop\n                                        continue;\n                                    }\n                                }\n                            }\n\n                            if (child.Children is not null)\n                                break;\n                        }\n                    }\n\n                    if (child is not null)\n                    {\n                        //make found child as current\n                        k = 0;\n                        currentNode = child;\n                        continue; //start over\n                    }\n                }\n\n                //no child nodes available; move up to parent node\n                k = currentNode.K + 1;\n                currentNode = currentNode.Parent;\n            }\n\n            return null;\n        }\n\n        private static bool SubDomainExists(byte[] key, Node currentNode)\n        {\n            Node[] children = currentNode.Children;\n            if (children is not null)\n            {\n                Node child = Volatile.Read(ref children[1]); //[*]\n                if (child is not null)\n                    return true; //wildcard exists so subdomain name exists: RFC 4592 section 4.9\n            }\n\n            Node nextSubDomain = GetNextSubDomainZoneNode(key, currentNode, currentNode.Depth);\n            if (nextSubDomain is null)\n                return false;\n\n            NodeValue value = nextSubDomain.Value;\n            if (value is null)\n                return false;\n\n            return IsKeySubDomain(key, value.Key, false);\n        }\n\n        private static AuthZone GetAuthZoneFromNode(Node node, string zoneName)\n        {\n            NodeValue value = node.Value;\n            if (value is not null)\n            {\n                AuthZoneNode authZoneNode = value.Value;\n                if (authZoneNode is not null)\n                    return authZoneNode.GetAuthZone(zoneName);\n            }\n\n            return null;\n        }\n\n        private void RemoveAllSubDomains(string domain, Node currentNode)\n        {\n            //remove all sub domains under current zone\n            Node current = currentNode;\n            byte[] currentKey = ConvertToByteKey(domain);\n\n            do\n            {\n                current = GetNextSubDomainZoneNode(currentKey, current, currentNode.Depth);\n                if (current is null)\n                    break;\n\n                NodeValue v = current.Value;\n                if (v is not null)\n                {\n                    AuthZoneNode z = v.Value;\n                    if (z is not null)\n                    {\n                        if (z.ApexZone is null)\n                        {\n                            //no apex zone at this node; remove complete zone node\n                            current.RemoveNodeValue(v.Key, out _); //remove node value\n                            current.CleanThisBranch();\n                        }\n                        else\n                        {\n                            //apex node exists; remove parent size sub domain\n                            z.TryRemove(out SubDomainZone _);\n                        }\n                    }\n\n                    currentKey = v.Key;\n                }\n            }\n            while (true);\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override void GetClosestValuesForZone(AuthZoneNode zoneValue, out SubDomainZone closestSubDomain, out SubDomainZone closestDelegation, out ApexZone closestAuthority)\n        {\n            ApexZone apexZone = zoneValue.ApexZone;\n            if (apexZone is not null)\n            {\n                //hosted primary/secondary/stub/forwarder zone found\n                closestSubDomain = null;\n                closestDelegation = zoneValue.ParentSideZone;\n                closestAuthority = apexZone;\n            }\n            else\n            {\n                //hosted sub domain\n                SubDomainZone subDomainZone = zoneValue.ParentSideZone;\n\n                if (subDomainZone.ContainsNameServerRecords())\n                {\n                    //delegated sub domain found\n                    closestSubDomain = null;\n                    closestDelegation = subDomainZone;\n                }\n                else\n                {\n                    closestSubDomain = subDomainZone;\n                    closestDelegation = null;\n                }\n\n                closestAuthority = null;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public bool TryAdd(ApexZone zone)\n        {\n            AuthZoneNode zoneNode = GetOrAdd(zone.Name, delegate (string key)\n            {\n                return new AuthZoneNode(null, zone);\n            });\n\n            if (ReferenceEquals(zoneNode.ApexZone, zone))\n                return true; //added successfully\n\n            return zoneNode.TryAdd(zone);\n        }\n\n        public bool TryGet(string zoneName, string domain, out AuthZone authZone)\n        {\n            if (TryGet(domain, out AuthZoneNode authZoneNode))\n            {\n                authZone = authZoneNode.GetAuthZone(zoneName);\n                return authZone is not null;\n            }\n\n            authZone = null;\n            return false;\n        }\n\n        public bool TryGet(string zoneName, out ApexZone apexZone)\n        {\n            if (TryGet(zoneName, out AuthZoneNode authZoneNode) && (authZoneNode.ApexZone is not null))\n            {\n                apexZone = authZoneNode.ApexZone;\n                return true;\n            }\n\n            apexZone = null;\n            return false;\n        }\n\n        public bool TryRemove(string domain, out ApexZone apexZone)\n        {\n            if (!TryGet(domain, out AuthZoneNode authZoneNode, out Node currentNode) || (authZoneNode.ApexZone is null))\n            {\n                apexZone = null;\n                return false;\n            }\n\n            apexZone = authZoneNode.ApexZone;\n\n            if (authZoneNode.ParentSideZone is null)\n            {\n                //remove complete zone node\n                if (!base.TryRemove(domain, out AuthZoneNode _))\n                {\n                    apexZone = null;\n                    return false;\n                }\n            }\n            else\n            {\n                //parent side sub domain exists; remove only apex zone from zone node\n                if (!authZoneNode.TryRemove(out ApexZone _))\n                {\n                    apexZone = null;\n                    return false;\n                }\n            }\n\n            //remove all sub domains under current apex zone\n            RemoveAllSubDomains(domain, currentNode);\n\n            currentNode.CleanThisBranch();\n            return true;\n        }\n\n        public bool TryRemove(string domain, out SubDomainZone subDomainZone, bool removeAllSubDomains = false)\n        {\n            if (!TryGet(domain, out AuthZoneNode zoneNode, out Node currentNode) || (zoneNode.ParentSideZone is null))\n            {\n                subDomainZone = null;\n                return false;\n            }\n\n            subDomainZone = zoneNode.ParentSideZone;\n\n            if (zoneNode.ApexZone is null)\n            {\n                //remove complete zone node\n                if (!base.TryRemove(domain, out AuthZoneNode _))\n                {\n                    subDomainZone = null;\n                    return false;\n                }\n            }\n            else\n            {\n                //apex zone exists; remove only parent side sub domain from zone node\n                if (!zoneNode.TryRemove(out SubDomainZone _))\n                {\n                    subDomainZone = null;\n                    return false;\n                }\n            }\n\n            if (removeAllSubDomains)\n                RemoveAllSubDomains(domain, currentNode); //remove all sub domains under current subdomain zone\n\n            currentNode.CleanThisBranch();\n            return true;\n        }\n\n        public override bool TryRemove(string key, out AuthZoneNode authZoneNode)\n        {\n            throw new InvalidOperationException();\n        }\n\n        public IReadOnlyList<AuthZone> GetApexZoneWithSubDomainZones(string zoneName)\n        {\n            List<AuthZone> zones = new List<AuthZone>();\n\n            byte[] key = ConvertToByteKey(zoneName);\n\n            NodeValue nodeValue = _root.FindNodeValue(key, out Node currentNode);\n            if (nodeValue is not null)\n            {\n                AuthZoneNode authZoneNode = nodeValue.Value;\n                if (authZoneNode is not null)\n                {\n                    ApexZone apexZone = authZoneNode.ApexZone;\n                    if (apexZone is not null)\n                    {\n                        zones.Add(apexZone);\n\n                        Node current = currentNode;\n                        byte[] currentKey = key;\n\n                        do\n                        {\n                            current = GetNextSubDomainZoneNode(currentKey, current, currentNode.Depth);\n                            if (current is null)\n                                break;\n\n                            NodeValue value = current.Value;\n                            if (value is not null)\n                            {\n                                authZoneNode = value.Value;\n                                if (authZoneNode is not null)\n                                    zones.Add(authZoneNode.ParentSideZone);\n\n                                currentKey = value.Key;\n                            }\n                        }\n                        while (true);\n                    }\n                }\n            }\n\n            return zones;\n        }\n\n        public IReadOnlyList<AuthZone> GetSubDomainZoneWithSubDomainZones(string domain)\n        {\n            List<AuthZone> zones = new List<AuthZone>();\n\n            byte[] key = ConvertToByteKey(domain);\n\n            NodeValue nodeValue = _root.FindNodeValue(key, out Node currentNode);\n            if (nodeValue is not null)\n            {\n                AuthZoneNode authZoneNode = nodeValue.Value;\n                if (authZoneNode is not null)\n                {\n                    SubDomainZone subDomainZone = authZoneNode.ParentSideZone;\n                    if (subDomainZone is not null)\n                    {\n                        zones.Add(subDomainZone);\n\n                        Node current = currentNode;\n                        byte[] currentKey = key;\n\n                        do\n                        {\n                            current = GetNextSubDomainZoneNode(currentKey, current, currentNode.Depth);\n                            if (current is null)\n                                break;\n\n                            NodeValue value = current.Value;\n                            if (value is not null)\n                            {\n                                authZoneNode = value.Value;\n                                if (authZoneNode is not null)\n                                    zones.Add(authZoneNode.ParentSideZone);\n\n                                currentKey = value.Key;\n                            }\n                        }\n                        while (true);\n                    }\n                }\n            }\n\n            return zones;\n        }\n\n        public AuthZone GetOrAddSubDomainZone(string zoneName, string domain, Func<SubDomainZone> valueFactory)\n        {\n            bool isApex = zoneName.Equals(domain, StringComparison.OrdinalIgnoreCase);\n\n            AuthZoneNode authZoneNode = GetOrAdd(domain, delegate (string key)\n            {\n                if (isApex)\n                    throw new DnsServerException(\"Zone was not found for domain: \" + key);\n\n                return new AuthZoneNode(valueFactory(), null);\n            });\n\n            if (isApex)\n            {\n                if (authZoneNode.ApexZone is null)\n                    throw new DnsServerException(\"Zone was not found: \" + zoneName);\n\n                return authZoneNode.ApexZone;\n            }\n            else\n            {\n                return authZoneNode.GetOrAddParentSideZone(valueFactory);\n            }\n        }\n\n        public AuthZone GetAuthZone(string zoneName, string domain)\n        {\n            if (TryGet(domain, out AuthZoneNode authZoneNode))\n                return authZoneNode.GetAuthZone(zoneName);\n\n            return null;\n        }\n\n        public ApexZone GetApexZone(string zoneName)\n        {\n            if (TryGet(zoneName, out AuthZoneNode authZoneNode))\n                return authZoneNode.ApexZone;\n\n            return null;\n        }\n\n        public AuthZone FindZone(string domain, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone authority, out bool hasSubDomains)\n        {\n            byte[] key = ConvertToByteKey(domain);\n\n            AuthZoneNode authZoneNode = FindZoneNode(key, true, out Node currentNode, out Node closestSubDomainNode, out _, out SubDomainZone closestSubDomain, out SubDomainZone closestDelegation, out ApexZone closestAuthority);\n            if (authZoneNode is null)\n            {\n                //zone not found\n                closest = closestSubDomain;\n                delegation = closestDelegation;\n                authority = closestAuthority;\n\n                if (authority is null)\n                {\n                    //no authority so no sub domains\n                    hasSubDomains = false;\n                }\n                else if ((closestSubDomainNode is not null) && !closestSubDomainNode.HasChildren)\n                {\n                    //closest sub domain node does not have any children so no sub domains\n                    hasSubDomains = false;\n                }\n                else\n                {\n                    //check if current node has sub domains\n                    hasSubDomains = SubDomainExists(key, currentNode);\n                }\n\n                return null;\n            }\n            else\n            {\n                //zone found\n                AuthZone zone;\n\n                ApexZone apexZone = authZoneNode.ApexZone;\n                if (apexZone is not null)\n                {\n                    zone = apexZone;\n                    closest = null;\n                    delegation = authZoneNode.ParentSideZone;\n                    authority = apexZone;\n                }\n                else\n                {\n                    SubDomainZone subDomainZone = authZoneNode.ParentSideZone;\n\n                    zone = subDomainZone;\n\n                    if (zone == closestSubDomain)\n                        closest = null;\n                    else\n                        closest = closestSubDomain;\n\n                    if (closestDelegation is not null)\n                        delegation = closestDelegation;\n                    else if (subDomainZone.ContainsNameServerRecords())\n                        delegation = subDomainZone;\n                    else\n                        delegation = null;\n\n                    authority = closestAuthority;\n                }\n\n                if (zone.Disabled)\n                {\n                    if ((closestSubDomainNode is not null) && !closestSubDomainNode.HasChildren)\n                    {\n                        //closest sub domain node does not have any children so no sub domains\n                        hasSubDomains = false;\n                    }\n                    else\n                    {\n                        //check if current node has sub domains\n                        hasSubDomains = SubDomainExists(key, currentNode);\n                    }\n                }\n                else\n                {\n                    //since zone is found, it does not matter if subdomain exists or not\n                    hasSubDomains = false;\n                }\n\n                return zone;\n            }\n        }\n\n        public AuthZone FindPreviousSubDomainZone(string zoneName, string domain)\n        {\n            byte[] key = ConvertToByteKey(domain);\n\n            AuthZoneNode authZoneNode = FindZoneNode(key, false, out Node currentNode, out _, out Node closestAuthorityNode, out _, out _, out _);\n            if (authZoneNode is not null)\n            {\n                //zone exists\n                ApexZone apexZone = authZoneNode.ApexZone;\n                SubDomainZone parentSideZone = authZoneNode.ParentSideZone;\n\n                if ((apexZone is not null) && (parentSideZone is not null))\n                {\n                    //found ambiguity between apex zone and sub domain zone\n                    if (!apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    {\n                        //zone name does not match with apex zone and thus not match with closest authority node\n                        //find the closest authority zone for given zone name\n                        if (!TryGet(zoneName, out _, out Node closestNodeForZoneName))\n                            throw new InvalidOperationException();\n\n                        closestAuthorityNode = closestNodeForZoneName;\n                    }\n                }\n            }\n\n            Node previousNode = GetPreviousSubDomainZoneNode(key, currentNode, closestAuthorityNode.Depth);\n            if (previousNode is not null)\n            {\n                AuthZone authZone = GetAuthZoneFromNode(previousNode, zoneName);\n                if (authZone is not null)\n                    return authZone;\n            }\n\n            return null;\n        }\n\n        public AuthZone FindNextSubDomainZone(string zoneName, string domain)\n        {\n            byte[] key = ConvertToByteKey(domain);\n\n            AuthZoneNode authZoneNode = FindZoneNode(key, false, out Node currentNode, out _, out Node closestAuthorityNode, out _, out _, out _);\n            if (authZoneNode is not null)\n            {\n                //zone exists\n                ApexZone apexZone = authZoneNode.ApexZone;\n                SubDomainZone parentSideZone = authZoneNode.ParentSideZone;\n\n                if ((apexZone is not null) && (parentSideZone is not null))\n                {\n                    //found ambiguity between apex zone and sub domain zone\n                    if (!apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    {\n                        //zone name does not match with apex zone and thus not match with closest authority node\n                        //find the closest authority zone for given zone name\n                        if (!TryGet(zoneName, out _, out Node closestNodeForZoneName))\n                            throw new InvalidOperationException();\n\n                        closestAuthorityNode = closestNodeForZoneName;\n                    }\n                }\n            }\n\n            Node nextNode = GetNextSubDomainZoneNode(key, currentNode, closestAuthorityNode.Depth);\n            if (nextNode is not null)\n            {\n                AuthZone authZone = GetAuthZoneFromNode(nextNode, zoneName);\n                if (authZone is not null)\n                    return authZone;\n            }\n\n            return null;\n        }\n\n        public bool SubDomainExistsFor(string zoneName, string domain)\n        {\n            AuthZone nextAuthZone = FindNextSubDomainZone(zoneName, domain);\n            if (nextAuthZone is null)\n                return false;\n\n            return nextAuthZone.Name.EndsWith(\".\" + domain, StringComparison.OrdinalIgnoreCase);\n        }\n\n        #endregion\n\n        #region DNSSEC\n\n        public IReadOnlyList<DnsResourceRecord> FindNSecProofOfNonExistenceNxDomain(string domain, bool isWildcardAnswer)\n        {\n            List<DnsResourceRecord> nsecRecords = new List<DnsResourceRecord>(2 * 2);\n\n            //add proof of cover for domain\n            NSecAddProofOfCoverFor(domain, nsecRecords);\n\n            if (isWildcardAnswer)\n                return nsecRecords;\n\n            //add proof of cover for wildcard\n            if (nsecRecords.Count > 0)\n            {\n                //add wildcard proof to prove that a wildcard expansion was not possible\n                DnsResourceRecord nsecRecord = nsecRecords[0];\n                DnsNSECRecordData nsec = nsecRecord.RDATA as DnsNSECRecordData;\n                string wildcardName = DnsNSECRecordData.GetWildcardFor(nsecRecord, domain);\n\n                if (!DnsNSECRecordData.IsDomainCovered(nsecRecord.Name, nsec.NextDomainName, wildcardName))\n                    NSecAddProofOfCoverFor(wildcardName, nsecRecords);\n            }\n\n            return nsecRecords;\n        }\n\n        public IReadOnlyList<DnsResourceRecord> FindNSec3ProofOfNonExistenceNxDomain(string domain, bool isWildcardAnswer)\n        {\n            List<DnsResourceRecord> nsec3Records = new List<DnsResourceRecord>(3 * 2);\n\n            byte[] key = ConvertToByteKey(domain);\n            string closestEncloser;\n\n            AuthZoneNode authZoneNode = FindZoneNode(key, isWildcardAnswer, out _, out _, out _, out SubDomainZone closestSubDomain, out _, out ApexZone closestAuthority);\n            if (authZoneNode is not null)\n            {\n                if (isWildcardAnswer && (closestSubDomain is not null) && closestSubDomain.Name.StartsWith('*'))\n                {\n                    closestEncloser = closestSubDomain.Name.TrimStart(_starPeriodTrimChars);\n                }\n                else\n                {\n                    //subdomain that contains only NSEC3 record does not really exists: RFC5155 section 7.2.8    \n                    if ((authZoneNode.ApexZone is not null) || ((authZoneNode.ParentSideZone is not null) && !authZoneNode.ParentSideZone.HasOnlyNSec3Records()))\n                        throw new InvalidOperationException($\"Cannot prove non-existence: The domain name '{domain}' exists and probably got added just now.\"); //domain exists! cannot prove non-existence\n\n                    //continue to prove non-existence of this nsec3 owner name\n                    closestEncloser = closestAuthority.Name;\n                }\n            }\n            else\n            {\n                if (closestSubDomain is not null)\n                    closestEncloser = closestSubDomain.Name;\n                else if (closestAuthority is not null)\n                    closestEncloser = closestAuthority.Name;\n                else\n                    throw new InvalidOperationException(); //cannot find closest encloser\n            }\n\n            IReadOnlyList<DnsResourceRecord> nsec3ParamRecords = closestAuthority.GetRecords(DnsResourceRecordType.NSEC3PARAM);\n            if (nsec3ParamRecords.Count == 0)\n                throw new InvalidOperationException(\"Zone does not have NSEC3 deployed.\");\n\n            DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData;\n\n            //find correct closest encloser\n            string hashedNextCloserName;\n\n            while (true)\n            {\n                string nextCloserName = DnsNSEC3RecordData.GetNextCloserName(domain, closestEncloser);\n                hashedNextCloserName = nsec3Param.ComputeHashedOwnerNameBase32HexString(nextCloserName) + (closestAuthority.Name.Length > 0 ? \".\" + closestAuthority.Name : \"\");\n\n                AuthZone nsec3Zone = GetAuthZone(closestAuthority.Name, hashedNextCloserName);\n                if (nsec3Zone is null)\n                    break; //next closer name does not exists\n\n                //next closer name exists as an ENT\n                closestEncloser = nextCloserName;\n\n                if (domain.Equals(closestEncloser, StringComparison.OrdinalIgnoreCase))\n                {\n                    //domain exists as an ENT; return no data proof\n                    return FindNSec3ProofOfNonExistenceNoData(nsec3Zone);\n                }\n            }\n\n            if (isWildcardAnswer)\n            {\n                //add proof of cover for the domain to prove non-existence (wildcard)\n                NSec3AddProofOfCoverFor(hashedNextCloserName, closestAuthority.Name, nsec3Records);\n            }\n            else\n            {\n                //add closest encloser proof\n                string hashedClosestEncloser = nsec3Param.ComputeHashedOwnerNameBase32HexString(closestEncloser) + (closestAuthority.Name.Length > 0 ? \".\" + closestAuthority.Name : \"\");\n\n                AuthZone nsec3Zone = GetAuthZone(closestAuthority.Name, hashedClosestEncloser);\n                if (nsec3Zone is null)\n                    throw new InvalidOperationException();\n\n                IReadOnlyList<DnsResourceRecord> closestEncloserProofRecords = nsec3Zone.QueryRecords(DnsResourceRecordType.NSEC3, true);\n                if (closestEncloserProofRecords.Count == 0)\n                    throw new InvalidOperationException();\n\n                nsec3Records.AddRange(closestEncloserProofRecords);\n\n                DnsResourceRecord closestEncloserProofRecord = closestEncloserProofRecords[0];\n                DnsNSEC3RecordData closestEncloserProof = closestEncloserProofRecord.RDATA as DnsNSEC3RecordData;\n\n                //add proof of cover for the next closer name\n                if (!DnsNSECRecordData.IsDomainCovered(closestEncloserProofRecord.Name, closestEncloserProof.NextHashedOwnerName + (closestAuthority.Name.Length > 0 ? \".\" + closestAuthority.Name : \"\"), hashedNextCloserName))\n                    NSec3AddProofOfCoverFor(hashedNextCloserName, closestAuthority.Name, nsec3Records);\n\n                //add proof of cover to prove that a wildcard expansion was not possible\n                string wildcardDomain = closestEncloser.Length > 0 ? \"*.\" + closestEncloser : \"*\";\n                string hashedWildcardDomainName = nsec3Param.ComputeHashedOwnerNameBase32HexString(wildcardDomain) + (closestAuthority.Name.Length > 0 ? \".\" + closestAuthority.Name : \"\");\n\n                if (!DnsNSECRecordData.IsDomainCovered(closestEncloserProofRecord.Name, closestEncloserProof.NextHashedOwnerName + (closestAuthority.Name.Length > 0 ? \".\" + closestAuthority.Name : \"\"), hashedWildcardDomainName))\n                    NSec3AddProofOfCoverFor(hashedWildcardDomainName, closestAuthority.Name, nsec3Records);\n            }\n\n            return nsec3Records;\n        }\n\n        public IReadOnlyList<DnsResourceRecord> FindNSecProofOfNonExistenceNoData(string domain, AuthZone zone)\n        {\n            List<DnsResourceRecord> nsecRecords = null;\n\n            if (zone.Name.StartsWith(\"*.\") || zone.Name.Equals('*'))\n            {\n                //for wildcard case, we need to add proof of cover since validator wont be able to match qname to the NO DATA NSEC record\n                nsecRecords = new List<DnsResourceRecord>(4);\n\n                NSecAddProofOfCoverFor(domain, nsecRecords);\n            }\n\n            IReadOnlyList<DnsResourceRecord> nsecRecordsNoData = zone.QueryRecords(DnsResourceRecordType.NSEC, true);\n            if (nsecRecordsNoData.Count == 0)\n                throw new InvalidOperationException(\"Zone does not have NSEC deployed correctly.\");\n\n            if (nsecRecords is null)\n                return nsecRecordsNoData;\n\n            foreach (DnsResourceRecord nsecRecord in nsecRecordsNoData)\n            {\n                if (!nsecRecords.Contains(nsecRecord))\n                    nsecRecords.Add(nsecRecord);\n            }\n\n            return nsecRecords;\n        }\n\n        public IReadOnlyList<DnsResourceRecord> FindNSec3ProofOfNonExistenceNoData(string domain, AuthZone zone, ApexZone apexZone)\n        {\n            IReadOnlyList<DnsResourceRecord> nsec3ParamRecords = apexZone.GetRecords(DnsResourceRecordType.NSEC3PARAM);\n            if (nsec3ParamRecords.Count == 0)\n                throw new InvalidOperationException(\"Zone does not have NSEC3 deployed.\");\n\n            DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData;\n            List<DnsResourceRecord> nsec3Records = null;\n\n            if (zone.Name.StartsWith(\"*.\") || zone.Name.Equals('*'))\n            {\n                //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\n                string closestEncloser = AuthZoneManager.GetParentZone(zone.Name);\n                if (closestEncloser is null)\n                    closestEncloser = \"\";\n\n                string closestEncloserHashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(closestEncloser) + (apexZone.Name.Length > 0 ? \".\" + apexZone.Name : \"\");\n\n                AuthZone nsec3ZoneClosestEncloser = GetAuthZone(apexZone.Name, closestEncloserHashedOwnerName);\n                if (nsec3ZoneClosestEncloser is not null)\n                {\n                    nsec3Records = new List<DnsResourceRecord>(4);\n                    nsec3Records.AddRange(FindNSec3ProofOfNonExistenceNoData(nsec3ZoneClosestEncloser));\n\n                    string qnameHashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(domain) + (apexZone.Name.Length > 0 ? \".\" + apexZone.Name : \"\");\n                    NSec3AddProofOfCoverFor(qnameHashedOwnerName, apexZone.Name, nsec3Records);\n                }\n            }\n\n            string hashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(zone.Name) + (apexZone.Name.Length > 0 ? \".\" + apexZone.Name : \"\");\n\n            AuthZone nsec3Zone = GetAuthZone(apexZone.Name, hashedOwnerName);\n            if (nsec3Zone is null)\n            {\n                //this is probably since the domain in request is for an nsec3 record owner name\n                return FindNSec3ProofOfNonExistenceNxDomain(zone.Name, false);\n            }\n\n            IReadOnlyList<DnsResourceRecord> nsec3RecordsNoData = FindNSec3ProofOfNonExistenceNoData(nsec3Zone);\n\n            if (nsec3Records is null)\n                return nsec3RecordsNoData;\n\n            foreach (DnsResourceRecord nsec3Record in nsec3RecordsNoData)\n            {\n                if (!nsec3Records.Contains(nsec3Record))\n                    nsec3Records.Add(nsec3Record);\n            }\n\n            return nsec3Records;\n        }\n\n        private static IReadOnlyList<DnsResourceRecord> FindNSec3ProofOfNonExistenceNoData(AuthZone nsec3Zone)\n        {\n            IReadOnlyList<DnsResourceRecord> nsec3Records = nsec3Zone.QueryRecords(DnsResourceRecordType.NSEC3, true);\n            if (nsec3Records.Count > 0)\n                return nsec3Records;\n\n            return Array.Empty<DnsResourceRecord>();\n        }\n\n        private void NSecAddProofOfCoverFor(string domain, List<DnsResourceRecord> nsecRecords)\n        {\n            byte[] key = ConvertToByteKey(domain);\n\n            AuthZoneNode authZoneNode = FindZoneNode(key, false, out Node currentNode, out _, out Node closestAuthorityNode, out _, out _, out ApexZone closestAuthority);\n            if (authZoneNode is not null)\n                throw new InvalidOperationException($\"Cannot prove non-existence: The domain name '{domain}' exists and probably got added just now.\"); //domain exists! cannot prove non-existence\n\n            Node previousNode = GetPreviousSubDomainZoneNode(key, currentNode, closestAuthorityNode.Depth);\n            if (previousNode is not null)\n            {\n                AuthZone authZone = GetAuthZoneFromNode(previousNode, closestAuthority.Name);\n                if (authZone is not null)\n                {\n                    IReadOnlyList<DnsResourceRecord> proofOfCoverRecords = authZone.QueryRecords(DnsResourceRecordType.NSEC, true);\n\n                    foreach (DnsResourceRecord proofOfCoverRecord in proofOfCoverRecords)\n                    {\n                        if (!nsecRecords.Contains(proofOfCoverRecord))\n                            nsecRecords.Add(proofOfCoverRecord);\n                    }\n                }\n            }\n        }\n\n        private void NSec3AddProofOfCoverFor(string hashedOwnerName, string zoneName, List<DnsResourceRecord> nsec3Records)\n        {\n            IReadOnlyList<DnsResourceRecord> TryFindPreviousNSec3Records(string ownerName)\n            {\n                while (true)\n                {\n                    AuthZone zone = FindPreviousSubDomainZone(zoneName, ownerName);\n                    if (zone is null)\n                        return null; //no previous auth zone found\n\n                    IReadOnlyList<DnsResourceRecord> previousNSec3Records = zone.QueryRecords(DnsResourceRecordType.NSEC3, true);\n                    if (previousNSec3Records.Count > 0)\n                        return previousNSec3Records; //found proof of cover\n\n                    ownerName = zone.Name;\n                }\n            }\n\n            //find previous NSEC3 for the hashed owner name\n            IReadOnlyList<DnsResourceRecord> proofOfCoverRecords = TryFindPreviousNSec3Records(hashedOwnerName);\n\n            if (proofOfCoverRecords is null)\n            {\n                //didnt find previous NSEC3; find the last NSEC3 which will give the proof of cover\n                proofOfCoverRecords = TryFindPreviousNSec3Records(\"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz0\" + (zoneName.Length > 0 ? \".\" + zoneName : \"\"));\n            }\n\n            if (proofOfCoverRecords is null)\n                throw new InvalidOperationException();\n\n            foreach (DnsResourceRecord proofOfCoverRecord in proofOfCoverRecords)\n            {\n                if (!nsec3Records.Contains(proofOfCoverRecord))\n                    nsec3Records.Add(proofOfCoverRecord);\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Trees/CacheZoneTree.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2022  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Zones;\n\nnamespace DnsServerCore.Dns.Trees\n{\n    class CacheZoneTree : ZoneTree<CacheZone, CacheZone, CacheZone>\n    {\n        #region protected\n\n        protected override void GetClosestValuesForZone(CacheZone zoneValue, out CacheZone closestSubDomain, out CacheZone closestDelegation, out CacheZone closestAuthority)\n        {\n            if (zoneValue.ContainsNameServerRecords())\n            {\n                //ns records found\n                closestSubDomain = null;\n                closestDelegation = zoneValue;\n            }\n            else\n            {\n                closestSubDomain = zoneValue;\n                closestDelegation = null;\n            }\n\n            closestAuthority = null;\n        }\n\n        #endregion\n\n        #region public\n\n        public bool TryRemoveTree(string domain, out CacheZone value, out int removedEntries)\n        {\n            bool removed = TryRemove(domain, out value, out Node currentNode);\n            if (removed)\n                removedEntries = value.TotalEntries;\n            else\n                removedEntries = 0;\n\n            //remove all cache zones under current zone\n            Node current = currentNode;\n\n            do\n            {\n                current = current.GetNextNodeWithValue(currentNode.Depth);\n                if (current is null)\n                    break;\n\n                NodeValue v = current.Value;\n                if (v is not null)\n                {\n                    CacheZone zone = v.Value;\n                    if (zone is not null)\n                    {\n                        current.RemoveNodeValue(v.Key, out _); //remove node value\n                        current.CleanThisBranch();\n                        removed = true;\n                        removedEntries += zone.TotalEntries;\n                    }\n                }\n            }\n            while (true);\n\n            if (removed)\n                currentNode.CleanThisBranch();\n\n            return removed;\n        }\n\n        public CacheZone FindZone(string domain, out CacheZone closest, out CacheZone delegation)\n        {\n            byte[] key = ConvertToByteKey(domain);\n\n            CacheZone zoneValue = FindZoneNode(key, false, out _, out _, out _, out CacheZone closestSubDomain, out CacheZone closestDelegation, out _);\n            if (zoneValue is null)\n            {\n                //zone not found\n                closest = closestSubDomain; //required for DNAME\n                delegation = closestDelegation;\n\n                return null;\n            }\n            else\n            {\n                //zone found\n                closest = null; //not required\n\n                if (zoneValue.ContainsNameServerRecords())\n                    delegation = zoneValue;\n                else if (closestDelegation is not null)\n                    delegation = closestDelegation;\n                else\n                    delegation = null;\n\n                return zoneValue;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Trees/DomainTree.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Text;\nusing TechnitiumLibrary.ByteTree;\n\nnamespace DnsServerCore.Dns.Trees\n{\n    class DomainTree<T> : ByteTree<string, T> where T : class\n    {\n        #region variables\n\n        readonly static byte[] _keyMap;\n        readonly static byte[] _reverseKeyMap;\n\n        #endregion\n\n        #region constructor\n\n        static DomainTree()\n        {\n            _keyMap = new byte[256];\n            _reverseKeyMap = new byte[41];\n\n            int keyCode;\n\n            for (int i = 0; i < _keyMap.Length; i++)\n            {\n                if (i == 46) //[.]\n                {\n                    keyCode = 0;\n                    _keyMap[i] = (byte)keyCode;\n                    _reverseKeyMap[keyCode] = (byte)i;\n                }\n                else if (i == 42) //[*]\n                {\n                    keyCode = 1;\n                    _keyMap[i] = 0xff; //skipped value for optimization\n                    _reverseKeyMap[keyCode] = (byte)i;\n                }\n                else if (i == 45) //[-]\n                {\n                    keyCode = 2;\n                    _keyMap[i] = (byte)keyCode;\n                    _reverseKeyMap[keyCode] = (byte)i;\n                }\n                else if (i == 47) //[/]\n                {\n                    keyCode = 3;\n                    _keyMap[i] = (byte)keyCode;\n                    _reverseKeyMap[keyCode] = (byte)i;\n                }\n                else if ((i >= 48) && (i <= 57)) //[0-9]\n                {\n                    keyCode = i - 44; //4 - 13\n                    _keyMap[i] = (byte)keyCode;\n                    _reverseKeyMap[keyCode] = (byte)i;\n                }\n                else if (i == 95) //[_]\n                {\n                    keyCode = 14;\n                    _keyMap[i] = (byte)keyCode;\n                    _reverseKeyMap[keyCode] = (byte)i;\n                }\n                else if ((i >= 97) && (i <= 122)) //[a-z]\n                {\n                    keyCode = i - 82; //15 - 40\n                    _keyMap[i] = (byte)keyCode;\n                    _reverseKeyMap[keyCode] = (byte)i;\n                }\n                else if ((i >= 65) && (i <= 90)) //[A-Z]\n                {\n                    keyCode = i - 50; //15 - 40\n                    _keyMap[i] = (byte)keyCode;\n                }\n                else\n                {\n                    _keyMap[i] = 0xff;\n                }\n            }\n        }\n\n        public DomainTree()\n            : base(41)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override byte[] ConvertToByteKey(string domain, bool throwException = true)\n        {\n            if (domain.Length == 0)\n                return [];\n\n            if (domain.Length > 255)\n            {\n                if (throwException)\n                    throw new InvalidDomainNameException(\"Invalid domain name [\" + domain + \"]: length cannot exceed 255 bytes.\");\n\n                return null;\n            }\n\n            byte[] key = new byte[domain.Length + 1];\n            int keyOffset = 0;\n            int labelStart;\n            int labelEnd = domain.Length - 1;\n            int labelLength;\n            int labelChar;\n            byte labelKeyCode;\n            int i;\n\n            do\n            {\n                if (labelEnd < 0)\n                    labelEnd = 0;\n\n                labelStart = domain.LastIndexOf('.', labelEnd);\n                labelLength = labelEnd - labelStart;\n\n                if (labelLength == 0)\n                {\n                    if (throwException)\n                        throw new InvalidDomainNameException(\"Invalid domain name [\" + domain + \"]: label length cannot be 0 byte.\");\n\n                    return null;\n                }\n\n                if (labelLength > 63)\n                {\n                    if (throwException)\n                        throw new InvalidDomainNameException(\"Invalid domain name [\" + domain + \"]: label length cannot exceed 63 bytes.\");\n\n                    return null;\n                }\n\n                if (domain[labelStart + 1] == '-')\n                {\n                    if (throwException)\n                        throw new InvalidDomainNameException(\"Invalid domain name [\" + domain + \"]: label cannot start with hyphen.\");\n\n                    return null;\n                }\n\n                if (domain[labelEnd] == '-')\n                {\n                    if (throwException)\n                        throw new InvalidDomainNameException(\"Invalid domain name [\" + domain + \"]: label cannot end with hyphen.\");\n\n                    return null;\n                }\n\n                if ((labelLength == 1) && (domain[labelStart + 1] == '*')) //[*]\n                {\n                    key[keyOffset++] = 1;\n                }\n                else\n                {\n                    for (i = labelStart + 1; i <= labelEnd; i++)\n                    {\n                        labelChar = domain[i];\n                        if (labelChar >= _keyMap.Length)\n                        {\n                            if (throwException)\n                                throw new InvalidDomainNameException(\"Invalid domain name [\" + domain + \"]: invalid character [\" + labelChar + \"] was found.\");\n\n                            return null;\n                        }\n\n                        labelKeyCode = _keyMap[labelChar];\n                        if (labelKeyCode == 0xff)\n                        {\n                            if (throwException)\n                                throw new InvalidDomainNameException(\"Invalid domain name [\" + domain + \"]: invalid character [\" + labelChar + \"] was found.\");\n\n                            return null;\n                        }\n\n                        key[keyOffset++] = labelKeyCode;\n                    }\n                }\n\n                key[keyOffset++] = 0; //[.]\n                labelEnd = labelStart - 1;\n            }\n            while (labelStart > -1);\n\n            return key;\n        }\n\n        protected static string ConvertKeyToLabel(byte[] key, int startIndex)\n        {\n            int length = key.Length - startIndex;\n            if (length < 1)\n                return null;\n\n            Span<byte> domain = stackalloc byte[length];\n            int i;\n            int k;\n\n            for (i = 0; i < domain.Length; i++)\n            {\n                k = key[i + startIndex];\n                if (k == 0) //[.]\n                    break;\n\n                domain[i] = _reverseKeyMap[k];\n            }\n\n            return Encoding.ASCII.GetString(domain.Slice(0, i));\n        }\n\n        #endregion\n\n        #region public\n\n        public override bool TryRemove(string key, out T value)\n        {\n            if (TryRemove(key, out value, out Node currentNode))\n            {\n                currentNode.CleanThisBranch();\n                return true;\n            }\n\n            return false;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Trees/InvalidDomainNameException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore.Dns.Trees\n{\n    public class InvalidDomainNameException : DnsServerException\n    {\n        #region constructors\n\n        public InvalidDomainNameException()\n            : base()\n        { }\n\n        public InvalidDomainNameException(string message)\n            : base(message)\n        { }\n\n        public InvalidDomainNameException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Trees/ZoneTree.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Zones;\nusing System.Collections.Generic;\nusing System.Threading;\n\nnamespace DnsServerCore.Dns.Trees\n{\n    abstract class ZoneTree<TNode, TSubDomainZone, TApexZone> : DomainTree<TNode> where TNode : class where TSubDomainZone : Zone where TApexZone : Zone\n    {\n        #region private\n\n        private static Node GetNextChildZoneNode(Node current, int baseDepth)\n        {\n            int k = 0;\n\n            while ((current is not null) && (current.Depth >= baseDepth))\n            {\n                if ((current.K != 0) || (current.Depth == baseDepth)) //[.] skip this node's children as its last for current sub zone\n                {\n                    Node[] children = current.Children;\n                    if (children is not null)\n                    {\n                        //find child node\n                        Node child = null;\n\n                        for (int i = k; i < children.Length; i++)\n                        {\n                            child = Volatile.Read(ref children[i]);\n                            if (child is not null)\n                            {\n                                if (child.Value is not null)\n                                    return child; //child has value so return it\n\n                                if (child.K == 0) //[.]\n                                    return child; //child node is last for current sub zone\n\n                                if (child.Children is not null)\n                                    break;\n                            }\n                        }\n\n                        if (child is not null)\n                        {\n                            //make found child as current\n                            k = 0;\n                            current = child;\n                            continue; //start over\n                        }\n                    }\n                }\n\n                //no child nodes available; move up to parent node\n                k = current.K + 1;\n                current = current.Parent;\n            }\n\n            return null;\n        }\n\n        private static byte[] GetNodeKey(Node node)\n        {\n            byte[] key = new byte[node.Depth];\n            int i = node.Depth - 1;\n\n            while (i > -1)\n            {\n                key[i--] = node.K;\n                node = node.Parent;\n            }\n\n            return key;\n        }\n\n        private static bool KeysMatch(byte[] mainKey, byte[] testKey, bool matchWildcard)\n        {\n            if (matchWildcard)\n            {\n                //com.example.*.\n                //com.example.*.www.\n                //com.example.abc.www.\n\n                int i = 0;\n                int j = 0;\n\n                while ((i < mainKey.Length) && (j < testKey.Length))\n                {\n                    if ((mainKey[i] == 1) && (testKey[j] != 1)) //[*] wildcard match only when test key does not have '*' as literal char: RFC 4592 section 2.3\n                    {\n                        if (i == mainKey.Length - 2)\n                            return true; //last label, valid wildcard\n                    }\n\n                    if (mainKey[i] != testKey[j])\n                        return false;\n\n                    i++;\n                    j++;\n                }\n\n                return (i == mainKey.Length) && (j == testKey.Length);\n            }\n            else\n            {\n                //exact match\n                if (mainKey.Length != testKey.Length)\n                    return false;\n\n                for (int i = 0; i < mainKey.Length; i++)\n                {\n                    if (mainKey[i] != testKey[i])\n                        return false;\n                }\n\n                return true;\n            }\n        }\n\n        private void FindClosestValuesForZone(TNode zoneNode, Node currentNode, ref Node closestSubDomainNode, ref Node closestAuthorityNode, ref TSubDomainZone closestSubDomain, ref TSubDomainZone closestDelegation, ref TApexZone closestAuthority)\n        {\n            GetClosestValuesForZone(zoneNode, out TSubDomainZone subDomain, out TSubDomainZone delegation, out TApexZone authority);\n\n            if (subDomain is not null)\n            {\n                closestSubDomain = subDomain;\n                closestSubDomainNode = currentNode;\n            }\n\n            if (delegation is not null)\n                closestDelegation = delegation;\n\n            if (authority is not null)\n            {\n                closestAuthority = authority;\n                closestAuthorityNode = currentNode;\n\n                closestSubDomain = null; //clear previous closest sub domain\n                closestSubDomainNode = null;\n            }\n        }\n\n        #endregion\n\n        #region protected\n\n        protected static bool IsKeySubDomain(byte[] mainKey, byte[] testKey, bool matchWildcard)\n        {\n            if (matchWildcard)\n            {\n                //com.example.*.\n                //com.example.*.www.\n                //com.example.abc.www.\n\n                int i = 0;\n                int j = 0;\n\n                while ((i < mainKey.Length) && (j < testKey.Length))\n                {\n                    if ((mainKey[i] == 1) && (testKey[j] != 1)) //[*] wildcard match only when test key does not have '*' as literal char: RFC 4592 section 2.3\n                    {\n                        //skip j to next label\n                        while (j < testKey.Length)\n                        {\n                            if (testKey[j] == 0) //[.]\n                                break;\n\n                            j++;\n                        }\n\n                        i++;\n                        continue;\n                    }\n\n                    if (mainKey[i] != testKey[j])\n                        return false;\n\n                    i++;\n                    j++;\n                }\n\n                return (i == mainKey.Length) && (j < testKey.Length);\n            }\n            else\n            {\n                //exact match\n                if (mainKey.Length > testKey.Length)\n                    return false;\n\n                for (int i = 0; i < mainKey.Length; i++)\n                {\n                    if (mainKey[i] != testKey[i])\n                        return false;\n                }\n\n                return mainKey.Length < testKey.Length;\n            }\n        }\n\n        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)\n        {\n            currentNode = _root;\n            closestSubDomainNode = null;\n            closestAuthorityNode = null;\n            closestSubDomain = null;\n            closestDelegation = null;\n            closestAuthority = null;\n            Node wildcardNode = null;\n            int i = 0;\n\n            while (i <= key.Length)\n            {\n                //inspect the current node\n                NodeValue value = currentNode.Value;\n                if ((value is not null) && (value.Key.Length <= key.Length))\n                {\n                    TNode zoneNode = value.Value;\n                    if ((zoneNode is not null) && IsKeySubDomain(value.Key, key, matchWildcard))\n                    {\n                        FindClosestValuesForZone(zoneNode, currentNode, ref closestSubDomainNode, ref closestAuthorityNode, ref closestSubDomain, ref closestDelegation, ref closestAuthority);\n\n                        wildcardNode = null; //clear previous wildcard node\n                    }\n                }\n\n                if (i == key.Length)\n                    break;\n\n                Node[] children = currentNode.Children;\n                if (children is null)\n                    break;\n\n                Node childNode;\n\n                if (matchWildcard && (key[i] != 1)) //wildcard match only when key does not have '*' as literal char: RFC 4592 section 2.3\n                {\n                    childNode = Volatile.Read(ref children[1]); //[*]\n                    if (childNode is not null)\n                    {\n                        NodeValue wValue = childNode.Value;\n                        if (wValue is null)\n                        {\n                            //find value from next [.] node\n                            Node[] wChildren = childNode.Children;\n                            if (wChildren is not null)\n                            {\n                                Node wChildNode = Volatile.Read(ref wChildren[0]); //[.]\n                                if (wChildNode is not null)\n                                {\n                                    wValue = wChildNode.Value;\n                                    if ((wValue is not null) && (wValue.Key.Length == wChildNode.Depth))\n                                        wildcardNode = wChildNode;\n                                }\n                            }\n                        }\n                        else if (wValue.Key.Length == childNode.Depth + 1)\n                        {\n                            wildcardNode = childNode;\n                        }\n                    }\n                }\n\n                childNode = Volatile.Read(ref children[key[i]]);\n                if (childNode is null)\n                {\n                    //no child found\n                    if (wildcardNode is null)\n                        return null; //no child or wildcard found\n\n                    //use wildcard node\n                    break;\n                }\n\n                currentNode = childNode;\n                i++;\n            }\n\n            {\n                NodeValue value = currentNode.Value;\n                if (value is not null)\n                {\n                    //match exact only\n                    if (KeysMatch(value.Key, key, matchWildcard))\n                    {\n                        //find closest values since the matched zone may be apex zone\n                        TNode zoneNode = value.Value;\n                        if (zoneNode is not null)\n                            FindClosestValuesForZone(zoneNode, currentNode, ref closestSubDomainNode, ref closestAuthorityNode, ref closestSubDomain, ref closestDelegation, ref closestAuthority);\n\n                        return value.Value; //found matching value\n                    }\n\n                    if (wildcardNode is not null)\n                    {\n                        NodeValue wildcardValue = wildcardNode.Value;\n                        if (wildcardValue is not null)\n                        {\n                            if (IsKeySubDomain(key, value.Key, false) && IsKeySubDomain(wildcardValue.Key, value.Key, matchWildcard))\n                            {\n                                //value is a subdomain of an ENT so wildcard is not valid\n                                wildcardNode = null;\n                            }\n                        }\n                    }\n                }\n                else if ((wildcardNode is not null) && (currentNode.K == 0) && currentNode.HasChildren && (currentNode != wildcardNode.Parent))\n                {\n                    //ENT node with children so wildcard is not valid\n                    wildcardNode = null;\n                }\n            }\n\n            if (wildcardNode is not null)\n            {\n                //inspect wildcard node value\n                NodeValue value = wildcardNode.Value;\n                if (value is not null)\n                {\n                    //match wildcard keys\n                    if (KeysMatch(value.Key, key, true))\n                    {\n                        //find closest values\n                        TNode zoneNode = value.Value;\n                        if (zoneNode is not null)\n                            FindClosestValuesForZone(zoneNode, currentNode, ref closestSubDomainNode, ref closestAuthorityNode, ref closestSubDomain, ref closestDelegation, ref closestAuthority);\n\n                        return value.Value; //found matching wildcard value\n                    }\n                }\n            }\n\n            //value not found\n            return null;\n        }\n\n        protected abstract void GetClosestValuesForZone(TNode zoneValue, out TSubDomainZone closestSubDomain, out TSubDomainZone closestDelegation, out TApexZone closestAuthority);\n\n        #endregion\n\n        #region public\n\n        public void ListSubDomains(string domain, List<string> subDomains)\n        {\n            byte[] bKey = ConvertToByteKey(domain);\n\n            _ = _root.FindNodeValue(bKey, out Node currentNode);\n            Node current = currentNode;\n            NodeValue value;\n\n            do\n            {\n                value = current.Value;\n                if (value is not null)\n                {\n                    if (IsKeySubDomain(bKey, value.Key, false))\n                    {\n                        string label = ConvertKeyToLabel(value.Key, bKey.Length);\n                        if (label is not null)\n                            subDomains.Add(label);\n                    }\n                }\n                else if ((current.K == 0) && (current.Depth > currentNode.Depth)) //[.]\n                {\n                    byte[] nodeKey = GetNodeKey(current);\n                    if (IsKeySubDomain(bKey, nodeKey, false))\n                    {\n                        string label = ConvertKeyToLabel(nodeKey, bKey.Length);\n                        if (label is not null)\n                            subDomains.Add(label);\n                    }\n                }\n\n                current = GetNextChildZoneNode(current, currentNode.Depth);\n            }\n            while (current is not null);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ZoneManagers/AllowedZoneManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Threading;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ZoneManagers\n{\n    public sealed class AllowedZoneManager : IDisposable\n    {\n        #region variables\n\n        readonly DnsServer _dnsServer;\n\n        AuthZoneManager _zoneManager;\n\n        readonly DnsSOARecordDataExtended _soaRecord;\n        readonly DnsNSRecordDataExtended _nsRecord;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        #endregion\n\n        #region constructor\n\n        public AllowedZoneManager(DnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n\n            _zoneManager = new AuthZoneManager(_dnsServer);\n\n            _soaRecord = new DnsSOARecordDataExtended(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 900, 300, 604800, 60);\n            _nsRecord = new DnsNSRecordDataExtended(_dnsServer.ServerDomain);\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveZoneFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsServer.LogManager.Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                if (_pendingSave)\n                {\n                    try\n                    {\n                        SaveZoneFileInternal();\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                    finally\n                    {\n                        _pendingSave = false;\n                    }\n                }\n            }\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region zone file\n\n        public void LoadAllowedZoneFile()\n        {\n            string allowedZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"allowed.config\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(allowedZoneFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS);\n                }\n\n                _dnsServer.LogManager.Write(\"DNS Server allowed zone file was loaded: \" + allowedZoneFile);\n            }\n            catch (FileNotFoundException)\n            {\n                SaveZoneFileInternal();\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(\"DNS Server encountered an error while loading allowed zone file: \" + allowedZoneFile + \"\\r\\n\" + ex.ToString());\n            }\n        }\n\n        public void LoadAllowedZone(Stream s)\n        {\n            lock (_saveLock)\n            {\n                ReadConfigFrom(s);\n\n                SaveZoneFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void SaveZoneFileInternal()\n        {\n            string allowedZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"allowed.config\");\n\n            using (FileStream fS = new FileStream(allowedZoneFile, FileMode.Create, FileAccess.Write))\n            {\n                WriteConfigTo(fS);\n            }\n\n            _dnsServer.LogManager.Write(\"DNS Server allowed zone file was saved: \" + allowedZoneFile);\n        }\n\n        public void SaveZoneFile()\n        {\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void ReadConfigFrom(Stream s)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"AZ\") //format\n                throw new InvalidDataException(\"DnsServer allowed zone file format is invalid.\");\n\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    int length = bR.ReadInt32();\n                    int i = 0;\n\n                    AuthZoneManager zoneManager = new AuthZoneManager(_dnsServer);\n\n                    zoneManager.LoadSpecialPrimaryZones(delegate ()\n                    {\n                        if (i++ < length)\n                            return bR.ReadShortString();\n\n                        return null;\n                    }, _soaRecord, _nsRecord);\n\n                    _zoneManager = zoneManager;\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"DnsServer allowed zone file version not supported.\");\n            }\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            IReadOnlyList<AuthZoneInfo> allowedZones = _zoneManager.GetAllZones();\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"AZ\")); //format\n            bW.Write((byte)1); //version\n\n            bW.Write(allowedZones.Count);\n\n            foreach (AuthZoneInfo zone in allowedZones)\n                bW.WriteShortString(zone.Name);\n        }\n\n        #endregion\n\n        #region private\n\n        internal void UpdateServerDomain()\n        {\n            _soaRecord.UpdatePrimaryNameServerAndMinimum(_dnsServer.ServerDomain, _dnsServer.BlockingAnswerTtl);\n            _nsRecord.UpdateNameServer(_dnsServer.ServerDomain);\n        }\n\n        #endregion\n\n        #region public\n\n        public void ImportZones(string[] domains)\n        {\n            _zoneManager.LoadSpecialPrimaryZones(domains, _soaRecord, _nsRecord);\n        }\n\n        public bool AllowZone(string domain)\n        {\n            if (_zoneManager.CreateSpecialPrimaryZone(domain, _soaRecord, _nsRecord) != null)\n                return true;\n\n            return false;\n        }\n\n        public bool DeleteZone(string domain)\n        {\n            if (_zoneManager.DeleteZone(domain))\n                return true;\n\n            return false;\n        }\n\n        public void Flush()\n        {\n            _zoneManager.Flush();\n        }\n\n        public IReadOnlyList<AuthZoneInfo> GetAllZones()\n        {\n            return _zoneManager.GetAllZones();\n        }\n\n        public void ListAllRecords(string domain, List<DnsResourceRecord> records)\n        {\n            _zoneManager.ListAllRecords(domain, domain, records);\n        }\n\n        public void ListSubDomains(string domain, List<string> subDomains)\n        {\n            _zoneManager.ListSubDomains(domain, subDomains);\n        }\n\n        public bool IsAllowed(DnsDatagram request)\n        {\n            if (_zoneManager.TotalZones < 1)\n                return false;\n\n            return _zoneManager.Query(request, false) is not null;\n        }\n\n        #endregion\n\n        #region properties\n\n        public int TotalZonesAllowed\n        { get { return _zoneManager.TotalZones; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Dnssec;\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Trees;\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Runtime.CompilerServices;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ZoneManagers\n{\n    public sealed class AuthZoneManager : IDisposable\n    {\n        #region events\n\n        public event EventHandler<SecondaryCatalogEventArgs> SecondaryCatalogZoneAdded;\n        public event EventHandler<SecondaryCatalogEventArgs> SecondaryCatalogZoneRemoved;\n\n        #endregion\n\n        #region variables\n\n        readonly DnsServer _dnsServer;\n\n        string _serverDomain;\n        uint _defaultRecordTtl = 3600;\n        uint _defaultNsRecordTtl = 14400;\n        uint _defaultSoaRecordTtl = 900;\n        bool _useSoaSerialDateScheme;\n        uint _minSoaRefresh = 300;\n        uint _minSoaRetry = 300;\n\n        readonly AuthZoneTree _root = new AuthZoneTree();\n\n        readonly List<AuthZoneInfo> _zoneIndex = new List<AuthZoneInfo>(10);\n        readonly List<AuthZoneInfo> _catalogZoneIndex = new List<AuthZoneInfo>(2);\n        readonly ReaderWriterLockSlim _zoneIndexLock = new ReaderWriterLockSlim();\n\n        readonly object _saveLock = new object();\n        readonly Dictionary<string, object> _pendingSaveZones = new Dictionary<string, object>();\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        volatile int _updateServerDomainId;\n\n        #endregion\n\n        #region constructor\n\n        public AuthZoneManager(DnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n\n            _serverDomain = _dnsServer.ServerDomain;\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    List<string> failedZones = new List<string>();\n\n                    foreach (KeyValuePair<string, object> pendingSaveZone in _pendingSaveZones)\n                    {\n                        try\n                        {\n                            SaveZoneFileInternal(pendingSaveZone.Key);\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsServer.LogManager.Write(ex);\n\n                            failedZones.Add(pendingSaveZone.Key);\n                        }\n                    }\n\n                    _pendingSaveZones.Clear();\n\n                    foreach (string zoneName in failedZones)\n                        _pendingSaveZones.TryAdd(zoneName, null);\n\n                    if (_pendingSaveZones.Count > 0)\n                        _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        private void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                lock (_saveLock)\n                {\n                    _saveTimer?.Dispose();\n\n                    try\n                    {\n                        foreach (KeyValuePair<string, object> pendingSaveZone in _pendingSaveZones)\n                        {\n                            try\n                            {\n                                SaveZoneFileInternal(pendingSaveZone.Key);\n                            }\n                            catch (Exception ex)\n                            {\n                                _dnsServer.LogManager.Write(ex);\n                            }\n                        }\n                    }\n                    finally\n                    {\n                        _pendingSaveZones.Clear();\n                    }\n                }\n\n                foreach (AuthZoneNode zoneNode in _root)\n                    zoneNode.Dispose();\n\n                _zoneIndexLock.Dispose();\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region zone file serialization and loading\n\n        public void LoadAllZoneFiles()\n        {\n            string zonesFolder = Path.Combine(_dnsServer.ConfigFolder, \"zones\");\n            if (!Directory.Exists(zonesFolder))\n                Directory.CreateDirectory(zonesFolder);\n\n            //move zone files to new folder\n            {\n                string[] oldZoneFiles = Directory.GetFiles(_dnsServer.ConfigFolder, \"*.zone\");\n\n                foreach (string oldZoneFile in oldZoneFiles)\n                    File.Move(oldZoneFile, Path.Combine(zonesFolder, Path.GetFileName(oldZoneFile)));\n            }\n\n            //remove old internal zones files\n            {\n                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\"];\n\n                foreach (string oldZoneFile in oldZoneFiles)\n                {\n                    string filePath = Path.Combine(zonesFolder, oldZoneFile);\n\n                    if (File.Exists(filePath))\n                    {\n                        try\n                        {\n                            File.Delete(filePath);\n                        }\n                        catch\n                        { }\n                    }\n                }\n            }\n\n            //flush existing zones\n            Flush();\n\n            //load all internal zones\n            LoadAllInternalZones();\n\n            //load zone files\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                string[] zoneFiles = Directory.GetFiles(zonesFolder, \"*.zone\");\n\n                foreach (string zoneFile in zoneFiles)\n                {\n                    try\n                    {\n                        using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read))\n                        {\n                            AuthZoneInfo zoneInfo = LoadZoneFrom(fS, File.GetLastWriteTimeUtc(fS.SafeFileHandle));\n                            _zoneIndex.Add(zoneInfo);\n\n                            if (zoneInfo.Type == AuthZoneType.Catalog)\n                                _catalogZoneIndex.Add(zoneInfo);\n                        }\n\n                        _dnsServer.LogManager.Write(\"DNS Server successfully loaded zone file: \" + zoneFile);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server failed to load zone file: \" + zoneFile + \"\\r\\n\" + ex.ToString());\n                    }\n                }\n\n                _zoneIndex.Sort();\n                _catalogZoneIndex.Sort();\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n        }\n\n        private void LoadAllInternalZones()\n        {\n            {\n                CreateInternalPrimaryZone(\"localhost\");\n                SetRecord(\"localhost\", new DnsResourceRecord(\"localhost\", DnsResourceRecordType.A, DnsClass.IN, 3600, new DnsARecordData(IPAddress.Loopback)));\n                SetRecord(\"localhost\", new DnsResourceRecord(\"localhost\", DnsResourceRecordType.AAAA, DnsClass.IN, 3600, new DnsAAAARecordData(IPAddress.IPv6Loopback)));\n            }\n\n            {\n                string ptrDomain = \"0.in-addr.arpa\";\n\n                CreateInternalPrimaryZone(ptrDomain);\n            }\n\n            {\n                string ptrDomain = \"255.in-addr.arpa\";\n\n                CreateInternalPrimaryZone(ptrDomain);\n            }\n\n            {\n                string ptrZoneName = \"127.in-addr.arpa\";\n\n                CreateInternalPrimaryZone(ptrZoneName);\n                SetRecord(ptrZoneName, new DnsResourceRecord(\"1.0.0.127.in-addr.arpa\", DnsResourceRecordType.PTR, DnsClass.IN, 3600, new DnsPTRRecordData(\"localhost\")));\n            }\n\n            {\n                string ptrZoneName = IPAddress.IPv6Loopback.GetReverseDomain();\n\n                CreateInternalPrimaryZone(ptrZoneName);\n                SetRecord(ptrZoneName, new DnsResourceRecord(ptrZoneName, DnsResourceRecordType.PTR, DnsClass.IN, 3600, new DnsPTRRecordData(\"localhost\")));\n            }\n        }\n\n        private void SaveZoneFileInternal(string zoneName)\n        {\n            zoneName = zoneName.ToLowerInvariant();\n\n            using (MemoryStream mS = new MemoryStream())\n            {\n                //serialize zone\n                WriteZoneTo(zoneName, mS);\n\n                if (mS.Position == 0)\n                    return; //zone was not found\n\n                //write to zone file\n                mS.Position = 0;\n\n                using (FileStream fS = new FileStream(Path.Combine(_dnsServer.ConfigFolder, \"zones\", zoneName + \".zone\"), FileMode.Create, FileAccess.Write))\n                {\n                    mS.CopyTo(fS);\n                }\n            }\n\n            _dnsServer.LogManager.Write(\"Saved zone file for domain: \" + (zoneName == \"\" ? \"<root>\" : zoneName));\n        }\n\n        public void SaveZoneFile(string zoneName)\n        {\n            zoneName = zoneName.ToLowerInvariant();\n\n            lock (_saveLock)\n            {\n                if (!_pendingSaveZones.TryAdd(zoneName, null))\n                    return;\n\n                if (_pendingSaveZones.Count == 1)\n                    _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private static uint GetMinExpiryTtlFor(IReadOnlyList<DnsResourceRecord> records)\n        {\n            uint minExpiryTtl = 0u;\n\n            foreach (DnsResourceRecord record in records)\n            {\n                GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();\n                if (recordInfo.ExpiryTtl > 0u)\n                {\n                    uint pendingExpiryTtl = recordInfo.GetPendingExpiryTtl();\n                    if (pendingExpiryTtl == 0)\n                    {\n                        //expired record found; set 10 sec ttl for timer to delete it\n                        minExpiryTtl = 10;\n                    }\n                    else\n                    {\n                        if (minExpiryTtl == 0u)\n                            minExpiryTtl = pendingExpiryTtl;\n                        else\n                            minExpiryTtl = Math.Min(minExpiryTtl, pendingExpiryTtl);\n                    }\n                }\n            }\n\n            return minExpiryTtl;\n        }\n\n        private void LoadAndInitZone(AuthZoneInfo zoneInfo, IReadOnlyList<DnsResourceRecord> records)\n        {\n            ApexZone apexZone = zoneInfo.ApexZone;\n\n            //load records\n            foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> zoneEntry in DnsResourceRecord.GroupRecords(records))\n            {\n                if (apexZone.Name.Equals(zoneEntry.Key, StringComparison.OrdinalIgnoreCase))\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> rrsetEntry in zoneEntry.Value)\n                        apexZone.LoadRecords(rrsetEntry.Key, rrsetEntry.Value);\n                }\n                else\n                {\n                    ValidateIfDomainBelongsToZone(apexZone.Name, zoneEntry.Key);\n\n                    AuthZone authZone = GetOrAddSubDomainZone(apexZone.Name, zoneEntry.Key);\n\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> rrsetEntry in zoneEntry.Value)\n                        authZone.LoadRecords(rrsetEntry.Key, rrsetEntry.Value);\n\n                    if (authZone is SubDomainZone subDomainZone)\n                        subDomainZone.AutoUpdateState();\n                }\n            }\n\n            //update dnssec status\n            apexZone.UpdateDnssecStatus();\n\n            //init zone\n            switch (zoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                    {\n                        apexZone.TriggerNotify();\n\n                        uint minExpiryTtl = GetMinExpiryTtlFor(records);\n                        if (minExpiryTtl > 0u)\n                            apexZone.StartRecordExpiryTimer(minExpiryTtl);\n                    }\n                    break;\n\n                case AuthZoneType.Secondary:\n                    {\n                        SecondaryZone secondary = apexZone as SecondaryZone;\n\n                        DnsResourceRecord soaRecord = secondary.GetRecords(DnsResourceRecordType.SOA)[0];\n                        SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo();\n                        if (soaInfo.Version == 1)\n                        {\n                            secondary.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers;\n                            secondary.PrimaryZoneTransferProtocol = soaInfo.ZoneTransferProtocol;\n                            secondary.PrimaryZoneTransferTsigKeyName = soaInfo.TsigKeyName;\n                        }\n\n                        secondary.TriggerNotify();\n                        secondary.TriggerRefresh();\n                    }\n                    break;\n\n                case AuthZoneType.Stub:\n                    {\n                        StubZone stub = apexZone as StubZone;\n\n                        DnsResourceRecord soaRecord = stub.GetRecords(DnsResourceRecordType.SOA)[0];\n                        SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo();\n                        if (soaInfo.Version == 1)\n                            stub.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers;\n\n                        stub.TriggerRefresh();\n                    }\n                    break;\n\n                case AuthZoneType.Forwarder:\n                    {\n                        IReadOnlyList<DnsResourceRecord> soaRecords = apexZone.GetRecords(DnsResourceRecordType.SOA);\n                        if (soaRecords.Count == 0)\n                            (apexZone as ForwarderZone).InitZone();\n\n                        apexZone.TriggerNotify();\n\n                        uint minExpiryTtl = GetMinExpiryTtlFor(records);\n                        if (minExpiryTtl > 0u)\n                            apexZone.StartRecordExpiryTimer(minExpiryTtl);\n                    }\n                    break;\n\n                case AuthZoneType.SecondaryForwarder:\n                    {\n                        (apexZone as SecondaryZone).TriggerRefresh();\n                    }\n                    break;\n\n                case AuthZoneType.Catalog:\n                    {\n                        (apexZone as CatalogZone).BuildMembersIndex();\n                        apexZone.TriggerNotify();\n\n                        uint minExpiryTtl = GetMinExpiryTtlFor(records);\n                        if (minExpiryTtl > 0u)\n                            apexZone.StartRecordExpiryTimer(minExpiryTtl);\n                    }\n                    break;\n\n                case AuthZoneType.SecondaryCatalog:\n                    {\n                        (apexZone as SecondaryZone).TriggerRefresh();\n                        (apexZone as SecondaryCatalogZone).BuildMembersIndex();\n                    }\n                    break;\n            }\n        }\n\n        public AuthZoneInfo LoadZoneFrom(Stream s, DateTime lastModified)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"DZ\")\n                throw new InvalidDataException(\"DnsServer zone file format is invalid.\");\n\n            switch (bR.ReadByte())\n            {\n                case 2:\n                    {\n                        DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()];\n                        if (records.Length == 0)\n                            throw new InvalidDataException(\"Zone does not contain SOA record.\");\n\n                        DnsResourceRecord soaRecord = null;\n\n                        for (int i = 0; i < records.Length; i++)\n                        {\n                            records[i] = new DnsResourceRecord(s);\n\n                            if (records[i].Type == DnsResourceRecordType.SOA)\n                                soaRecord = records[i];\n                        }\n\n                        if (soaRecord == null)\n                            throw new InvalidDataException(\"Zone does not contain SOA record.\");\n\n                        //make zone info\n                        AuthZoneType zoneType;\n                        if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase))\n                            zoneType = AuthZoneType.Primary;\n                        else\n                            zoneType = AuthZoneType.Stub;\n\n                        AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, false);\n\n                        //create zone\n                        ApexZone apexZone = CreateEmptyApexZone(zoneInfo);\n                        zoneInfo = new AuthZoneInfo(apexZone);\n\n                        try\n                        {\n                            //load and init zone\n                            LoadAndInitZone(zoneInfo, records);\n                        }\n                        catch\n                        {\n                            DeleteZone(zoneInfo);\n                            throw;\n                        }\n\n                        return zoneInfo;\n                    }\n\n                case 3:\n                    {\n                        bool zoneDisabled = bR.ReadBoolean();\n                        DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()];\n                        if (records.Length == 0)\n                            throw new InvalidDataException(\"Zone does not contain SOA record.\");\n\n                        DnsResourceRecord soaRecord = null;\n\n                        for (int i = 0; i < records.Length; i++)\n                        {\n                            records[i] = new DnsResourceRecord(s);\n                            records[i].Tag = AuthRecordInfo.ReadGenericRecordInfoFrom(bR, records[i].Type);\n\n                            if (records[i].Type == DnsResourceRecordType.SOA)\n                                soaRecord = records[i];\n                        }\n\n                        if (soaRecord == null)\n                            throw new InvalidDataException(\"Zone does not contain SOA record.\");\n\n                        //make zone info\n                        AuthZoneType zoneType;\n                        if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase))\n                            zoneType = AuthZoneType.Primary;\n                        else\n                            zoneType = AuthZoneType.Stub;\n\n                        AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, zoneDisabled);\n\n                        //create zone\n                        ApexZone apexZone = CreateEmptyApexZone(zoneInfo);\n                        zoneInfo = new AuthZoneInfo(apexZone);\n\n                        try\n                        {\n                            //load and init zone\n                            LoadAndInitZone(zoneInfo, records);\n                        }\n                        catch\n                        {\n                            DeleteZone(zoneInfo);\n                            throw;\n                        }\n\n                        return zoneInfo;\n                    }\n\n                case 4:\n                    {\n                        //read zone info\n                        AuthZoneInfo zoneInfo = new AuthZoneInfo(bR, lastModified);\n\n                        //create zone\n                        ApexZone apexZone = CreateEmptyApexZone(zoneInfo);\n                        zoneInfo = new AuthZoneInfo(apexZone);\n\n                        try\n                        {\n                            //read all zone records\n                            DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()];\n                            if (records.Length < 1)\n                                throw new InvalidDataException(\"Failed to load DNS zone file: the zone file does not contain any records.\");\n\n                            for (int i = 0; i < records.Length; i++)\n                            {\n                                records[i] = new DnsResourceRecord(s);\n                                records[i].Tag = AuthRecordInfo.ReadGenericRecordInfoFrom(bR, records[i].Type);\n                            }\n\n                            //load and init zone\n                            LoadAndInitZone(zoneInfo, records);\n                        }\n                        catch\n                        {\n                            DeleteZone(zoneInfo);\n                            throw;\n                        }\n\n                        return zoneInfo;\n                    }\n\n                default:\n                    throw new InvalidDataException(\"DNS Zone file version not supported.\");\n            }\n        }\n\n        public void WriteZoneTo(string zoneName, Stream s)\n        {\n            AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName, true);\n            if (zoneInfo is null)\n                return;\n\n            //serialize zone\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"DZ\")); //format\n            bW.Write((byte)4); //version\n\n            //write zone info\n            if (zoneInfo.Internal)\n                throw new InvalidOperationException(\"Cannot save zones marked as internal.\");\n\n            zoneInfo.WriteTo(bW);\n\n            //write all zone records\n            List<DnsResourceRecord> records = new List<DnsResourceRecord>();\n            ListAllZoneRecords(zoneInfo.Name, records);\n\n            bW.Write(records.Count);\n\n            foreach (DnsResourceRecord record in records)\n            {\n                record.WriteTo(s);\n                record.GetAuthGenericRecordInfo().WriteTo(bW);\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        internal void TriggerUpdateServerDomain(bool useBlockingAnswerTtl = false)\n        {\n            int id = RandomNumberGenerator.GetInt32(int.MaxValue);\n            _updateServerDomainId = id;\n\n            ThreadPool.QueueUserWorkItem(delegate (object state)\n            {\n                string serverDomain = _dnsServer.ServerDomain;\n\n                //update authoritative zone SOA and NS records\n                try\n                {\n                    IReadOnlyList<AuthZoneInfo> zones = GetAllZones();\n\n                    foreach (AuthZoneInfo zone in zones)\n                    {\n                        if (_updateServerDomainId != id)\n                            return; //stop current update since another update has been triggerred\n\n                        if (zone.Type != AuthZoneType.Primary)\n                            continue;\n\n                        DnsResourceRecord record = zone.ApexZone.GetRecords(DnsResourceRecordType.SOA)[0];\n                        DnsSOARecordData soa = record.RDATA as DnsSOARecordData;\n\n                        uint ttl;\n                        uint minimum;\n\n                        if (useBlockingAnswerTtl)\n                        {\n                            ttl = _dnsServer.BlockingAnswerTtl;\n                            minimum = ttl;\n                        }\n                        else\n                        {\n                            ttl = record.TTL;\n                            minimum = soa.Minimum;\n                        }\n\n                        if (soa.PrimaryNameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase))\n                        {\n                            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)));\n\n                            //update NS records\n                            IReadOnlyList<DnsResourceRecord> nsResourceRecords = zone.ApexZone.GetRecords(DnsResourceRecordType.NS);\n\n                            foreach (DnsResourceRecord nsResourceRecord in nsResourceRecords)\n                            {\n                                if ((nsResourceRecord.RDATA as DnsNSRecordData).NameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase))\n                                {\n                                    UpdateRecord(zone.Name, nsResourceRecord, new DnsResourceRecord(nsResourceRecord.Name, nsResourceRecord.Type, nsResourceRecord.Class, nsResourceRecord.TTL, new DnsNSRecordData(serverDomain)) { Tag = nsResourceRecord.Tag });\n                                    break;\n                                }\n                            }\n\n                            if (zone.Internal)\n                                continue; //dont save internal zones to disk\n\n                            //save zone file\n                            SaveZoneFile(zone.Name);\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n\n                //update server domain\n                _serverDomain = serverDomain;\n            });\n        }\n\n        internal static string GetParentZone(string domain)\n        {\n            int i = domain.IndexOf('.');\n            if (i > -1)\n                return domain.Substring(i + 1);\n\n            //dont return root zone\n            return null;\n        }\n\n        internal static bool DomainBelongsToZone(string zoneName, string domain)\n        {\n            return domain.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || domain.EndsWith(\".\" + zoneName, StringComparison.OrdinalIgnoreCase) || (zoneName.Length == 0);\n        }\n\n        internal static void ValidateIfDomainBelongsToZone(string zoneName, string domain)\n        {\n            if (!DomainBelongsToZone(zoneName, domain))\n                throw new DnsServerException(\"The domain name '\" + domain + \"' does not belong to the zone: \" + zoneName);\n        }\n\n        #endregion\n\n        #region auth zone tree methods\n\n        private ApexZone CreateEmptyApexZone(AuthZoneInfo zoneInfo)\n        {\n            ApexZone apexZone;\n\n            switch (zoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                    apexZone = new PrimaryZone(_dnsServer, zoneInfo);\n                    break;\n\n                case AuthZoneType.Secondary:\n                    apexZone = new SecondaryZone(_dnsServer, zoneInfo);\n                    break;\n\n                case AuthZoneType.Stub:\n                    apexZone = new StubZone(_dnsServer, zoneInfo);\n                    break;\n\n                case AuthZoneType.Forwarder:\n                    apexZone = new ForwarderZone(_dnsServer, zoneInfo);\n                    break;\n\n                case AuthZoneType.SecondaryForwarder:\n                    apexZone = new SecondaryForwarderZone(_dnsServer, zoneInfo);\n                    break;\n\n                case AuthZoneType.Catalog:\n                    apexZone = new CatalogZone(_dnsServer, zoneInfo);\n                    break;\n\n                case AuthZoneType.SecondaryCatalog:\n                    SecondaryCatalogZone secondaryCatalogZone = new SecondaryCatalogZone(_dnsServer, zoneInfo);\n                    secondaryCatalogZone.ZoneAdded += SecondaryCatalogZoneAdded;\n                    secondaryCatalogZone.ZoneRemoved += SecondaryCatalogZoneRemoved;\n\n                    apexZone = secondaryCatalogZone;\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"DNS zone type not supported.\");\n            }\n\n            if (_root.TryAdd(apexZone))\n                return apexZone;\n\n            throw new DnsServerException(\"Zone already exists: \" + zoneInfo.DisplayName);\n        }\n\n        internal AuthZone GetOrAddSubDomainZone(string zoneName, string domain)\n        {\n            return _root.GetOrAddSubDomainZone(zoneName, domain, delegate ()\n            {\n                if (!_root.TryGet(zoneName, out ApexZone apexZone))\n                    throw new DnsServerException(\"Zone was not found for domain: \" + domain);\n\n                if (apexZone is PrimaryZone primaryZone)\n                    return new PrimarySubDomainZone(primaryZone, domain);\n                else if (apexZone is SecondaryCatalogZone secondaryCatalogZone)\n                    return new SecondaryCatalogSubDomainZone(secondaryCatalogZone, domain);\n                else if (apexZone is SecondaryZone secondaryZone)\n                    return new SecondarySubDomainZone(secondaryZone, domain);\n                else if (apexZone is CatalogZone catalogZone)\n                    return new CatalogSubDomainZone(catalogZone, domain);\n                else if (apexZone is ForwarderZone forwarderZone)\n                    return new ForwarderSubDomainZone(forwarderZone, domain);\n\n                throw new DnsServerException(\"Zone cannot have sub domains.\");\n            });\n        }\n\n        internal IReadOnlyList<AuthZone> GetApexZoneWithSubDomainZones(string zoneName)\n        {\n            return _root.GetApexZoneWithSubDomainZones(zoneName);\n        }\n\n        public AuthZoneInfo GetAuthZoneInfo(string zoneName, bool loadHistory = false)\n        {\n            if (_root.TryGet(zoneName, out AuthZoneNode authZoneNode) && (authZoneNode.ApexZone is not null))\n                return new AuthZoneInfo(authZoneNode.ApexZone, loadHistory);\n\n            return null;\n        }\n\n        public AuthZoneInfo FindAuthZoneInfo(string domain, bool loadHistory = false)\n        {\n            _ = _root.FindZone(domain, out _, out _, out ApexZone apexZone, out _);\n            if (apexZone is null)\n                return null;\n\n            return new AuthZoneInfo(apexZone, loadHistory);\n        }\n\n        internal AuthZone GetAuthZone(string zoneName, string domain)\n        {\n            return _root.GetAuthZone(zoneName, domain);\n        }\n\n        internal ApexZone GetApexZone(string zoneName)\n        {\n            return _root.GetApexZone(zoneName);\n        }\n\n        public bool NameExists(string zoneName, string domain)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, domain);\n\n            return _root.TryGet(zoneName, domain, out _);\n        }\n\n        internal AuthZone FindPreviousSubDomainZone(string zoneName, string domain)\n        {\n            return _root.FindPreviousSubDomainZone(zoneName, domain);\n        }\n\n        internal AuthZone FindNextSubDomainZone(string zoneName, string domain)\n        {\n            return _root.FindNextSubDomainZone(zoneName, domain);\n        }\n\n        public void ListSubDomains(string domain, List<string> subDomains)\n        {\n            _root.ListSubDomains(domain, subDomains);\n        }\n\n        internal bool SubDomainExistsFor(string zoneName, string domain)\n        {\n            return _root.SubDomainExistsFor(zoneName, domain);\n        }\n\n        internal void RemoveSubDomainZone(string domain, bool removeAllSubDomains = false)\n        {\n            _root.TryRemove(domain, out SubDomainZone _, removeAllSubDomains);\n        }\n\n        internal void Flush()\n        {\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                foreach (AuthZoneNode zoneNode in _root)\n                    zoneNode.Dispose();\n\n                _root.Clear();\n                _zoneIndex.Clear();\n                _catalogZoneIndex.Clear();\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n        }\n\n        #endregion\n\n        #region zone create / delete / convert / clone\n\n        internal AuthZoneInfo CreateSpecialPrimaryZone(string zoneName, DnsSOARecordData soaRecord, DnsNSRecordData ns)\n        {\n            PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        internal void LoadSpecialPrimaryZones(IReadOnlyList<string> zoneNames, DnsSOARecordData soaRecord, DnsNSRecordData ns)\n        {\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                foreach (string zoneName in zoneNames)\n                {\n                    PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns);\n\n                    if (_root.TryAdd(apexZone))\n                    {\n                        AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                        _zoneIndex.Add(zoneInfo);\n                    }\n                }\n\n                _zoneIndex.Sort();\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n        }\n\n        internal void LoadSpecialPrimaryZones(Func<string> getZoneName, DnsSOARecordData soaRecord, DnsNSRecordData ns)\n        {\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                string zoneName;\n\n                while (true)\n                {\n                    zoneName = getZoneName();\n                    if (zoneName is null)\n                        break;\n\n                    PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns);\n\n                    if (_root.TryAdd(apexZone))\n                    {\n                        AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                        _zoneIndex.Add(zoneInfo);\n                    }\n                }\n\n                _zoneIndex.Sort();\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n        }\n\n        internal AuthZoneInfo CreateInternalPrimaryZone(string zoneName)\n        {\n            return CreatePrimaryZone(zoneName, true, _useSoaSerialDateScheme);\n        }\n\n        public AuthZoneInfo CreatePrimaryZone(string zoneName)\n        {\n            return CreatePrimaryZone(zoneName, false, _useSoaSerialDateScheme);\n        }\n\n        public AuthZoneInfo CreatePrimaryZone(string zoneName, bool useSoaSerialDateScheme)\n        {\n            return CreatePrimaryZone(zoneName, false, useSoaSerialDateScheme);\n        }\n\n        private AuthZoneInfo CreatePrimaryZone(string zoneName, bool @internal, bool useSoaSerialDateScheme)\n        {\n            PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, @internal, useSoaSerialDateScheme);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    if (!@internal)\n                        SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public Task<AuthZoneInfo> CreateSecondaryZoneAsync(string zoneName, string primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false)\n        {\n            NameServerAddress[] primaryNameServers;\n\n            if (string.IsNullOrEmpty(primaryNameServerAddresses))\n                primaryNameServers = null;\n            else\n                primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ',');\n\n            return CreateSecondaryZoneAsync(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, ignoreSoaFailure);\n        }\n\n        public async Task<AuthZoneInfo> CreateSecondaryZoneAsync(string zoneName, IReadOnlyList<NameServerAddress> primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false)\n        {\n            SecondaryZone apexZone = await SecondaryZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, ignoreSoaFailure);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    apexZone.TriggerRefresh(0);\n\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public Task<AuthZoneInfo> CreateStubZoneAsync(string zoneName, string primaryNameServerAddresses = null, bool ignoreSoaFailure = false)\n        {\n            NameServerAddress[] primaryNameServers;\n\n            if (string.IsNullOrEmpty(primaryNameServerAddresses))\n                primaryNameServers = null;\n            else\n                primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ',');\n\n            return CreateStubZoneAsync(zoneName, primaryNameServers, ignoreSoaFailure);\n        }\n\n        public async Task<AuthZoneInfo> CreateStubZoneAsync(string zoneName, IReadOnlyList<NameServerAddress> primaryNameServerAddresses = null, bool ignoreSoaFailure = false)\n        {\n            StubZone apexZone = await StubZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, ignoreSoaFailure);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    apexZone.TriggerRefresh(0);\n\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public AuthZoneInfo CreateForwarderZone(string zoneName)\n        {\n            ForwarderZone apexZone = new ForwarderZone(_dnsServer, zoneName);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public AuthZoneInfo CreateForwarderZone(string zoneName, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, DnsForwarderRecordProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments)\n        {\n            ForwarderZone apexZone = new ForwarderZone(_dnsServer, zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, fwdRecordComments);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public AuthZoneInfo CreateSecondaryForwarderZone(string zoneName, string primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null)\n        {\n            NameServerAddress[] primaryNameServers;\n\n            if (string.IsNullOrEmpty(primaryNameServerAddresses))\n                primaryNameServers = null;\n            else\n                primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ',');\n\n            return CreateSecondaryForwarderZone(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName);\n        }\n\n        public AuthZoneInfo CreateSecondaryForwarderZone(string zoneName, IReadOnlyList<NameServerAddress> primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null)\n        {\n            SecondaryForwarderZone apexZone = new SecondaryForwarderZone(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    apexZone.TriggerRefresh(0);\n\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public AuthZoneInfo CreateCatalogZone(string zoneName)\n        {\n            CatalogZone apexZone = new CatalogZone(_dnsServer, zoneName);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    _catalogZoneIndex.Add(zoneInfo);\n                    _catalogZoneIndex.Sort();\n\n                    apexZone.InitZoneProperties();\n\n                    SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public AuthZoneInfo CreateSecondaryCatalogZone(string zoneName, string primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null)\n        {\n            NameServerAddress[] primaryNameServers;\n\n            if (string.IsNullOrEmpty(primaryNameServerAddresses))\n                primaryNameServers = null;\n            else\n                primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ',');\n\n            return CreateSecondaryCatalogZone(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName);\n        }\n\n        public AuthZoneInfo CreateSecondaryCatalogZone(string zoneName, IReadOnlyList<NameServerAddress> primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null)\n        {\n            SecondaryCatalogZone apexZone = new SecondaryCatalogZone(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName);\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryAdd(apexZone))\n                {\n                    apexZone.ZoneAdded += SecondaryCatalogZoneAdded;\n                    apexZone.ZoneRemoved += SecondaryCatalogZoneRemoved;\n                    apexZone.TriggerRefresh(0);\n\n                    AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n                    _zoneIndex.Add(zoneInfo);\n                    _zoneIndex.Sort();\n\n                    SaveZoneFile(zoneInfo.Name);\n\n                    return zoneInfo;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return null;\n        }\n\n        public bool DeleteZone(string zoneName, bool deleteZoneFile = false)\n        {\n            AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName);\n            if (zoneInfo is null)\n                return false;\n\n            return DeleteZone(zoneInfo, deleteZoneFile);\n        }\n\n        public bool DeleteZone(AuthZoneInfo zoneInfo, bool deleteZoneFile = false)\n        {\n            return DeleteZone(zoneInfo, deleteZoneFile, false);\n        }\n\n        private bool DeleteZone(AuthZoneInfo zoneInfo, bool deleteZoneFile, bool skipCatalogMemberZoneProcessing)\n        {\n            if (!skipCatalogMemberZoneProcessing)\n            {\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Catalog:\n                        //update all zone memberships for catalog zone to be deleted\n                        foreach (string memberZoneName in (zoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames())\n                        {\n                            AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName);\n                            if (memberZoneInfo is null)\n                                continue;\n\n                            if (zoneInfo.Name.Equals(memberZoneInfo.CatalogZoneName, StringComparison.OrdinalIgnoreCase))\n                            {\n                                memberZoneInfo.ApexZone.CatalogZoneName = null;\n                                SaveZoneFile(memberZoneInfo.Name);\n                            }\n                        }\n                        break;\n\n                    case AuthZoneType.SecondaryCatalog:\n                        //delete all member zones for secondary catalog zone to be deleted\n                        foreach (string memberZoneName in (zoneInfo.ApexZone as SecondaryCatalogZone).GetAllMemberZoneNames())\n                        {\n                            AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName);\n                            if (memberZoneInfo is null)\n                                continue;\n\n                            if (zoneInfo.Name.Equals(memberZoneInfo.CatalogZoneName, StringComparison.OrdinalIgnoreCase))\n                                DeleteZone(memberZoneInfo, true);\n                        }\n                        break;\n                }\n            }\n\n            _zoneIndexLock.EnterWriteLock();\n            try\n            {\n                if (_root.TryRemove(zoneInfo.Name, out ApexZone removedApexZone))\n                {\n                    removedApexZone.Dispose();\n\n                    _zoneIndex.Remove(zoneInfo);\n\n                    if (zoneInfo.Type == AuthZoneType.Catalog)\n                        _catalogZoneIndex.Remove(zoneInfo);\n\n                    if (zoneInfo.CatalogZoneName is not null)\n                        RemoveCatalogMemberZone(zoneInfo); //remove catalog zone membership\n\n                    if (deleteZoneFile)\n                    {\n                        File.Delete(Path.Combine(_dnsServer.ConfigFolder, \"zones\", zoneInfo.Name + \".zone\"));\n\n                        _dnsServer.LogManager.Write(\"Deleted zone file for domain: \" + zoneInfo.DisplayName);\n                    }\n\n                    return true;\n                }\n            }\n            finally\n            {\n                _zoneIndexLock.ExitWriteLock();\n            }\n\n            return false;\n        }\n\n        public AuthZoneInfo CloneZone(string zoneName, string sourceZoneName)\n        {\n            AuthZoneInfo sourceZoneInfo = GetAuthZoneInfo(sourceZoneName);\n            if (sourceZoneInfo is null)\n                throw new DnsServerException(\"No such zone was found: \" + (sourceZoneName.Length == 0 ? \".\" : sourceZoneName));\n\n            AuthZoneInfo zoneInfo;\n\n            switch (sourceZoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                    zoneInfo = CreatePrimaryZone(zoneName);\n                    break;\n\n                case AuthZoneType.Forwarder:\n                    zoneInfo = CreateForwarderZone(zoneName);\n                    break;\n\n                default:\n                    throw new DnsServerException(\"Cannot clone the zone: source zone must be a Primary or Conditional Forwarder zone.\");\n            }\n\n            if (zoneInfo is null)\n                throw new DnsServerException(\"Failed to clone the zone: zone already exists.\");\n\n            //copy zone options\n            zoneInfo.Disabled = sourceZoneInfo.Disabled;\n\n            if (zoneInfo.Type == AuthZoneType.Primary)\n            {\n                zoneInfo.ZoneTransfer = sourceZoneInfo.ZoneTransfer;\n                zoneInfo.ZoneTransferNetworkACL = sourceZoneInfo.ZoneTransferNetworkACL;\n                zoneInfo.ZoneTransferTsigKeyNames = sourceZoneInfo.ZoneTransferTsigKeyNames;\n\n                zoneInfo.Notify = sourceZoneInfo.Notify;\n                zoneInfo.NotifyNameServers = sourceZoneInfo.NotifyNameServers;\n\n                zoneInfo.Update = sourceZoneInfo.Update;\n                zoneInfo.UpdateNetworkACL = sourceZoneInfo.UpdateNetworkACL;\n\n                if (sourceZoneInfo.UpdateSecurityPolicies is not null)\n                {\n                    Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicies = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>>(sourceZoneInfo.UpdateSecurityPolicies.Count);\n\n                    foreach (KeyValuePair<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> sourceSecurityPolicy in sourceZoneInfo.UpdateSecurityPolicies)\n                    {\n                        Dictionary<string, IReadOnlyList<DnsResourceRecordType>> policyMap = new Dictionary<string, IReadOnlyList<DnsResourceRecordType>>();\n\n                        foreach (KeyValuePair<string, IReadOnlyList<DnsResourceRecordType>> sourcePolicyMap in sourceSecurityPolicy.Value)\n                            policyMap.Add(string.Concat(sourcePolicyMap.Key.AsSpan(0, sourcePolicyMap.Key.Length - sourceZoneName.Length), zoneName), sourcePolicyMap.Value);\n\n                        updateSecurityPolicies.Add(sourceSecurityPolicy.Key, policyMap);\n                    }\n\n                    zoneInfo.UpdateSecurityPolicies = updateSecurityPolicies;\n                }\n            }\n\n            //copy records\n            List<DnsResourceRecord> sourceRecords = new List<DnsResourceRecord>();\n            ListAllZoneRecords(sourceZoneName, sourceRecords);\n\n            List<DnsResourceRecord> newRecords = new List<DnsResourceRecord>(sourceRecords.Count);\n\n            foreach (DnsResourceRecord sourceRecord in sourceRecords)\n            {\n                switch (sourceRecord.Type)\n                {\n                    case DnsResourceRecordType.DNSKEY:\n                    case DnsResourceRecordType.RRSIG:\n                    case DnsResourceRecordType.NSEC:\n                    case DnsResourceRecordType.NSEC3:\n                    case DnsResourceRecordType.NSEC3PARAM:\n                    case DnsResourceRecordType.DS:\n                        continue; //skip DNSSEC records\n\n                    default:\n                        DnsResourceRecord newRecord = new DnsResourceRecord(string.Concat(sourceRecord.Name.AsSpan(0, sourceRecord.Name.Length - sourceZoneName.Length), zoneName), sourceRecord.Type, sourceRecord.Class, sourceRecord.TTL, sourceRecord.RDATA);\n\n                        if (sourceRecord.Tag is NSRecordInfo nsInfo)\n                        {\n                            NSRecordInfo nrInfo = new NSRecordInfo();\n\n                            nrInfo.Disabled = nsInfo.Disabled;\n                            nrInfo.Comments = nsInfo.Comments;\n                            nrInfo.GlueRecords = nsInfo.GlueRecords;\n\n                            newRecord.Tag = nrInfo;\n                        }\n                        else if (sourceRecord.Tag is SOARecordInfo soaInfo)\n                        {\n                            SOARecordInfo nrInfo = new SOARecordInfo();\n\n                            nrInfo.Disabled = soaInfo.Disabled;\n                            nrInfo.Comments = soaInfo.Comments;\n                            nrInfo.UseSoaSerialDateScheme = soaInfo.UseSoaSerialDateScheme;\n\n                            newRecord.Tag = nrInfo;\n                        }\n                        else if (sourceRecord.Tag is SVCBRecordInfo svcbInfo)\n                        {\n                            SVCBRecordInfo nrInfo = new SVCBRecordInfo();\n\n                            nrInfo.Disabled = svcbInfo.Disabled;\n                            nrInfo.Comments = svcbInfo.Comments;\n                            nrInfo.AutoIpv4Hint = svcbInfo.AutoIpv4Hint;\n                            nrInfo.AutoIpv6Hint = svcbInfo.AutoIpv6Hint;\n\n                            newRecord.Tag = nrInfo;\n                        }\n                        else if (sourceRecord.Tag is GenericRecordInfo srInfo)\n                        {\n                            GenericRecordInfo nrInfo = new GenericRecordInfo();\n\n                            nrInfo.Disabled = srInfo.Disabled;\n                            nrInfo.Comments = srInfo.Comments;\n\n                            newRecord.Tag = nrInfo;\n                        }\n\n                        newRecords.Add(newRecord);\n                        break;\n                }\n            }\n\n            //load and init zone\n            LoadAndInitZone(zoneInfo, newRecords);\n\n            //save zone file\n            SaveZoneFile(zoneInfo.Name);\n\n            return zoneInfo;\n        }\n\n        public AuthZoneInfo ConvertZoneTypeTo(string zoneName, AuthZoneType newType)\n        {\n            AuthZoneInfo currentZoneInfo = GetAuthZoneInfo(zoneName);\n            if (currentZoneInfo is null)\n                throw new DnsServerException(\"No such zone was found: \" + (zoneName.Length == 0 ? \".\" : zoneName));\n\n            //validate conversion type\n            if (currentZoneInfo.Type == newType)\n                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.\");\n\n            switch (currentZoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                    switch (newType)\n                    {\n                        case AuthZoneType.Forwarder:\n                            if (currentZoneInfo.ApexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                                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.\");\n\n                            break;\n\n                        default:\n                            throw new DnsServerException(\"Cannot convert the zone '\" + currentZoneInfo.DisplayName + \"' from \" + currentZoneInfo.TypeName + \" to \" + AuthZoneInfo.GetZoneTypeName(newType) + \" zone: not supported.\");\n                    }\n\n                    break;\n\n                case AuthZoneType.Secondary:\n                case AuthZoneType.SecondaryForwarder:\n                case AuthZoneType.SecondaryCatalog:\n                    switch (newType)\n                    {\n                        case AuthZoneType.Primary:\n                        case AuthZoneType.Forwarder:\n                        case AuthZoneType.Catalog:\n                            break;\n\n                        default:\n                            throw new DnsServerException(\"Cannot convert the zone '\" + currentZoneInfo.DisplayName + \"' from \" + currentZoneInfo.TypeName + \" to \" + AuthZoneInfo.GetZoneTypeName(newType) + \" zone: not supported.\");\n                    }\n\n                    break;\n\n                case AuthZoneType.Forwarder:\n                    switch (newType)\n                    {\n                        case AuthZoneType.Primary:\n                            break;\n\n                        default:\n                            throw new DnsServerException(\"Cannot convert the zone '\" + currentZoneInfo.DisplayName + \"' from \" + currentZoneInfo.TypeName + \" to \" + AuthZoneInfo.GetZoneTypeName(newType) + \" zone: not supported.\");\n                    }\n\n                    break;\n\n                default:\n                    throw new DnsServerException(\"Cannot convert the zone '\" + currentZoneInfo.DisplayName + \"' from \" + currentZoneInfo.TypeName + \" to \" + AuthZoneInfo.GetZoneTypeName(newType) + \" zone: not supported.\");\n            }\n\n            return ConvertZoneTypeTo(currentZoneInfo, newType);\n        }\n\n        private AuthZoneInfo ConvertZoneTypeTo(AuthZoneInfo currentZoneInfo, AuthZoneType newType)\n        {\n            //read all current records\n            List<DnsResourceRecord> allRecords = new List<DnsResourceRecord>();\n            ListAllZoneRecords(currentZoneInfo.Name, allRecords);\n\n            try\n            {\n                //delete current zone from auth tree\n                DeleteZone(currentZoneInfo, false, true);\n\n                //create new zone\n                AuthZoneInfo newZoneInfo;\n\n                switch (newType)\n                {\n                    case AuthZoneType.Primary:\n                        switch (currentZoneInfo.Type)\n                        {\n                            case AuthZoneType.Secondary:\n                                {\n                                    //reset SOA metadata and remove DNSSEC records\n                                    List<DnsResourceRecord> updateRecords = new List<DnsResourceRecord>(allRecords.Count);\n\n                                    foreach (DnsResourceRecord record in allRecords)\n                                    {\n                                        switch (record.Type)\n                                        {\n                                            case DnsResourceRecordType.SOA:\n                                                {\n                                                    GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();\n                                                    record.Tag = null;\n\n                                                    GenericRecordInfo newRecordInfo = record.GetAuthGenericRecordInfo();\n                                                    newRecordInfo.Comments = recordInfo.Comments;\n                                                }\n                                                break;\n\n                                            case DnsResourceRecordType.DNSKEY:\n                                            case DnsResourceRecordType.RRSIG:\n                                            case DnsResourceRecordType.NSEC:\n                                            case DnsResourceRecordType.NSEC3:\n                                            case DnsResourceRecordType.NSEC3PARAM:\n                                                continue;\n                                        }\n\n                                        updateRecords.Add(record);\n                                    }\n\n                                    allRecords = updateRecords;\n                                }\n                                break;\n\n                            case AuthZoneType.Forwarder:\n                            case AuthZoneType.SecondaryForwarder:\n                                {\n                                    //remove all FWD records\n                                    List<DnsResourceRecord> updateRecords = new List<DnsResourceRecord>(allRecords.Count);\n\n                                    foreach (DnsResourceRecord record in allRecords)\n                                    {\n                                        if (record.Type == DnsResourceRecordType.FWD)\n                                            continue;\n\n                                        updateRecords.Add(record);\n                                    }\n\n                                    allRecords = updateRecords;\n                                }\n                                break;\n                        }\n\n                        newZoneInfo = CreatePrimaryZone(currentZoneInfo.Name);\n                        break;\n\n                    case AuthZoneType.Forwarder:\n                        switch (currentZoneInfo.Type)\n                        {\n                            case AuthZoneType.Primary:\n                            case AuthZoneType.SecondaryForwarder:\n                                {\n                                    //remove SOA and NS records\n                                    List<DnsResourceRecord> updateRecords = new List<DnsResourceRecord>(allRecords.Count);\n\n                                    foreach (DnsResourceRecord record in allRecords)\n                                    {\n                                        switch (record.Type)\n                                        {\n                                            case DnsResourceRecordType.SOA:\n                                            case DnsResourceRecordType.NS:\n                                                continue;\n                                        }\n\n                                        updateRecords.Add(record);\n                                    }\n\n                                    allRecords = updateRecords;\n                                }\n                                break;\n\n                            case AuthZoneType.Secondary:\n                                {\n                                    //remove SOA, NS and DNSSEC records\n                                    List<DnsResourceRecord> updateRecords = new List<DnsResourceRecord>(allRecords.Count);\n\n                                    foreach (DnsResourceRecord record in allRecords)\n                                    {\n                                        switch (record.Type)\n                                        {\n                                            case DnsResourceRecordType.SOA:\n                                            case DnsResourceRecordType.NS:\n                                            case DnsResourceRecordType.DNSKEY:\n                                            case DnsResourceRecordType.RRSIG:\n                                            case DnsResourceRecordType.NSEC:\n                                            case DnsResourceRecordType.NSEC3:\n                                            case DnsResourceRecordType.NSEC3PARAM:\n                                            case DnsResourceRecordType.DS:\n                                                continue;\n                                        }\n\n                                        updateRecords.Add(record);\n                                    }\n\n                                    allRecords = updateRecords;\n                                }\n                                break;\n                        }\n\n                        newZoneInfo = CreateForwarderZone(currentZoneInfo.Name);\n                        break;\n\n                    case AuthZoneType.Catalog:\n                        newZoneInfo = CreateCatalogZone(currentZoneInfo.Name);\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n\n                //load and init zone\n                LoadAndInitZone(newZoneInfo, allRecords);\n\n                //save zone file\n                SaveZoneFile(newZoneInfo.Name);\n\n                //post processing for catalog zones\n                if (newType == AuthZoneType.Catalog)\n                {\n                    //convert all member zones too\n                    CatalogZone newCatalogZone = newZoneInfo.ApexZone as CatalogZone;\n\n                    foreach (string memberZoneName in newCatalogZone.GetAllMemberZoneNames())\n                    {\n                        AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName);\n                        if (memberZoneInfo is null)\n                            continue;\n\n                        switch (memberZoneInfo.Type)\n                        {\n                            case AuthZoneType.Secondary:\n                                try\n                                {\n                                    AuthZoneType originalMemberZoneType = newCatalogZone.GetZoneTypeProperty(memberZoneInfo.Name);\n                                    if (originalMemberZoneType != AuthZoneType.Primary)\n                                    {\n                                        memberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name; //reset catalog zone object reference\n                                        break;\n                                    }\n\n                                    AuthZoneInfo newMemberZoneInfo = ConvertZoneTypeTo(memberZoneInfo, AuthZoneType.Primary);\n                                    newMemberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name;\n\n                                    AuthZoneDnssecStatus dnssecStatus = memberZoneInfo.ApexZone.DnssecStatus;\n                                    if (dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                                    {\n                                        //sign the new primary zone if the secondary zone was signed\n                                        SecondaryZone secondaryZone = memberZoneInfo.ApexZone as SecondaryZone;\n\n                                        IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys = secondaryZone.DnssecPrivateKeys;\n                                        if (dnssecPrivateKeys is not null)\n                                        {\n                                            try\n                                            {\n                                                IReadOnlyList<DnsResourceRecord> existingDnsKeyRecords = secondaryZone.GetRecords(DnsResourceRecordType.DNSKEY);\n\n                                                uint dnsKeyTtl = existingDnsKeyRecords[0].OriginalTtlValue;\n                                                bool useNSec3 = dnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3;\n                                                ushort iterations = 0;\n                                                byte[] salt = [];\n\n                                                if (useNSec3)\n                                                {\n                                                    IReadOnlyList<DnsResourceRecord> existingNsec3ParamRecord = secondaryZone.GetRecords(DnsResourceRecordType.NSEC3PARAM);\n                                                    DnsNSEC3PARAMRecordData nsec3Param = existingNsec3ParamRecord[0].RDATA as DnsNSEC3PARAMRecordData;\n\n                                                    iterations = nsec3Param.Iterations;\n                                                    salt = nsec3Param.Salt;\n                                                }\n\n                                                PrimaryZone newPrimaryZone = newMemberZoneInfo.ApexZone as PrimaryZone;\n                                                newPrimaryZone.SignZone(dnssecPrivateKeys, dnsKeyTtl, useNSec3, iterations, salt);\n                                            }\n                                            catch (Exception ex)\n                                            {\n                                                _dnsServer.LogManager.Write(ex);\n                                            }\n                                        }\n                                    }\n\n                                    SaveZoneFile(newMemberZoneInfo.Name);\n                                }\n                                catch\n                                {\n                                    //ignore errors since they were already logged\n                                }\n                                break;\n\n                            case AuthZoneType.SecondaryForwarder:\n                                try\n                                {\n                                    AuthZoneInfo newMemberZoneInfo = ConvertZoneTypeTo(memberZoneInfo, AuthZoneType.Forwarder);\n                                    newMemberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name;\n\n                                    SaveZoneFile(newMemberZoneInfo.Name);\n                                }\n                                catch\n                                {\n                                    //ignore errors since they were already logged\n                                }\n                                break;\n\n                            default: //stub zone\n                                memberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name; //reset catalog zone object reference\n                                break;\n                        }\n                    }\n                }\n\n                return newZoneInfo;\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(\"DNS Server failed to convert the zone '\" + currentZoneInfo.DisplayName + \"' from \" + currentZoneInfo.TypeName + \" to \" + AuthZoneInfo.GetZoneTypeName(newType) + \" zone.\\r\\n\" + ex.ToString());\n\n                //delete the zone if it was created\n                DeleteZone(currentZoneInfo);\n\n                //reload old zone file\n                string zoneFile = Path.Combine(_dnsServer.ConfigFolder, \"zones\", currentZoneInfo.Name + \".zone\");\n\n                _zoneIndexLock.EnterWriteLock();\n                try\n                {\n                    using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read))\n                    {\n                        AuthZoneInfo zoneInfo = LoadZoneFrom(fS, File.GetLastWriteTimeUtc(fS.SafeFileHandle));\n                        _zoneIndex.Add(zoneInfo);\n                        _zoneIndex.Sort();\n                    }\n\n                    _dnsServer.LogManager.Write(\"DNS Server successfully loaded zone file: \" + zoneFile);\n                }\n                catch (Exception ex2)\n                {\n                    _dnsServer.LogManager.Write(\"DNS Server failed to load zone file: \" + zoneFile + \"\\r\\n\" + ex2.ToString());\n                }\n                finally\n                {\n                    _zoneIndexLock.ExitWriteLock();\n                }\n\n                throw;\n            }\n        }\n\n        #endregion\n\n        #region catalog member zones\n\n        public void AddCatalogMemberZone(string catalogZoneName, AuthZoneInfo memberZoneInfo, bool ignoreValidationErrors = false)\n        {\n            switch (memberZoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                case AuthZoneType.Secondary:\n                case AuthZoneType.Stub:\n                case AuthZoneType.Forwarder:\n                    if (!ignoreValidationErrors)\n                    {\n                        string currentCatalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName;\n                        if (currentCatalogZoneName is not null)\n                            throw new DnsServerException(\"The zone '\" + memberZoneInfo.DisplayName + \"' is already a member of Catalog zone '\" + currentCatalogZoneName + \"'.\");\n                    }\n\n                    ApexZone apexZone = _root.GetApexZone(catalogZoneName);\n                    if (apexZone is not CatalogZone catalogZone)\n                    {\n                        if (ignoreValidationErrors)\n                            return;\n\n                        throw new DnsServerException(\"No such Catalog zone was found: \" + catalogZoneName);\n                    }\n\n                    //set catalog zone name in member zone so that properties can be set below correctly\n                    memberZoneInfo.ApexZone.CatalogZoneName = catalogZone.Name;\n\n                    if (!memberZoneInfo.Disabled)\n                    {\n                        catalogZone.AddMemberZone(memberZoneInfo.Name, memberZoneInfo.Type);\n\n                        //update properties in catalog zone by settings member zone property values again\n                        switch (memberZoneInfo.Type)\n                        {\n                            case AuthZoneType.Primary:\n                                memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess;\n                                memberZoneInfo.ZoneTransfer = memberZoneInfo.ZoneTransfer;\n                                memberZoneInfo.ZoneTransferTsigKeyNames = memberZoneInfo.ZoneTransferTsigKeyNames;\n                                break;\n\n                            case AuthZoneType.Secondary:\n                                memberZoneInfo.PrimaryNameServerAddresses = memberZoneInfo.PrimaryNameServerAddresses;\n                                memberZoneInfo.PrimaryZoneTransferProtocol = memberZoneInfo.PrimaryZoneTransferProtocol;\n                                memberZoneInfo.PrimaryZoneTransferTsigKeyName = memberZoneInfo.PrimaryZoneTransferTsigKeyName;\n                                memberZoneInfo.ValidateZone = memberZoneInfo.ValidateZone;\n                                memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess;\n                                memberZoneInfo.ZoneTransfer = memberZoneInfo.ZoneTransfer;\n                                memberZoneInfo.ZoneTransferTsigKeyNames = memberZoneInfo.ZoneTransferTsigKeyNames;\n                                break;\n\n                            case AuthZoneType.Stub:\n                                memberZoneInfo.PrimaryNameServerAddresses = memberZoneInfo.PrimaryNameServerAddresses;\n                                memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess;\n                                break;\n\n                            case AuthZoneType.Forwarder:\n                                memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess;\n                                break;\n                        }\n\n                        //save catalog changes\n                        SaveZoneFile(catalogZone.Name);\n                    }\n\n                    break;\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public void RemoveCatalogMemberZone(AuthZoneInfo memberZoneInfo, bool disableOnly = false)\n        {\n            switch (memberZoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                case AuthZoneType.Secondary:\n                case AuthZoneType.Stub:\n                case AuthZoneType.Forwarder:\n                case AuthZoneType.SecondaryForwarder:\n                    string catalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName;\n                    if (catalogZoneName is null)\n                        return;\n\n                    CatalogZone catalogZone = memberZoneInfo.ApexZone.CatalogZone;\n                    if (catalogZone is not null)\n                    {\n                        catalogZone.RemoveMemberZone(memberZoneInfo.Name);\n\n                        //save catalog changes\n                        SaveZoneFile(catalogZone.Name);\n                    }\n\n                    if (!disableOnly)\n                        memberZoneInfo.ApexZone.CatalogZoneName = null;\n\n                    break;\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        public void ChangeCatalogMemberZoneOwnership(AuthZoneInfo memberZoneInfo, string newCatalogZoneName)\n        {\n            switch (memberZoneInfo.Type)\n            {\n                case AuthZoneType.Primary:\n                case AuthZoneType.Secondary:\n                case AuthZoneType.Stub:\n                case AuthZoneType.Forwarder:\n                    string currentCatalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName;\n                    if (currentCatalogZoneName is null)\n                        throw new DnsServerException(\"The zone '\" + memberZoneInfo.DisplayName + \"' is not a member of any Catalog zone.\");\n\n                    AddCatalogMemberZone(newCatalogZoneName, memberZoneInfo, true);\n\n                    if (!memberZoneInfo.Disabled)\n                    {\n                        ApexZone apexZone = _root.GetApexZone(currentCatalogZoneName);\n                        if (apexZone is CatalogZone currentCatalogZone)\n                        {\n                            currentCatalogZone.ChangeMemberZoneOwnership(memberZoneInfo.Name, newCatalogZoneName);\n\n                            //save catalog changes\n                            SaveZoneFile(currentCatalogZone.Name);\n                        }\n                    }\n\n                    break;\n\n                default:\n                    throw new NotSupportedException();\n            }\n        }\n\n        #endregion\n\n        #region DNSSEC\n\n        public void SignPrimaryZone(string zoneName, DnssecPrivateKey kskPrivateKey, DnssecPrivateKey zskPrivateKey, uint dnsKeyTtl, bool useNSec3, ushort iterations = 0, byte saltLength = 0)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.SignZone(kskPrivateKey, zskPrivateKey, dnsKeyTtl, useNSec3, iterations, saltLength);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void UnsignPrimaryZone(string zoneName)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.UnsignZone();\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void ConvertPrimaryZoneToNSEC(string zoneName)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.ConvertToNSec();\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void ConvertPrimaryZoneToNSEC3(string zoneName, ushort iterations, byte saltLength)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.ConvertToNSec3(iterations, saltLength);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void UpdatePrimaryZoneNSEC3Parameters(string zoneName, ushort iterations, byte saltLength)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.UpdateNSec3Parameters(iterations, saltLength);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void UpdatePrimaryZoneDnsKeyTtl(string zoneName, uint dnsKeyTtl)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.UpdateDnsKeyTtl(dnsKeyTtl);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public DnssecPrivateKey GenerateAndAddPrimaryZoneDnssecPrivateKey(string zoneName, DnssecPrivateKeyType keyType, DnssecAlgorithm algorithm, ushort rolloverDays, int keySize = -1)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            DnssecPrivateKey privateKey = primaryZone.GenerateAndAddPrivateKey(keyType, algorithm, rolloverDays, keySize);\n\n            SaveZoneFile(primaryZone.Name);\n\n            return privateKey;\n        }\n\n        public void AddPrimaryZoneDnssecPrivateKey(string zoneName, DnssecPrivateKey privateKey)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.AddPrivateKey(privateKey);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public DnssecPrivateKey UpdatePrimaryZoneDnssecPrivateKey(string zoneName, ushort keyTag, ushort rolloverDays)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            DnssecPrivateKey privateKey = primaryZone.UpdatePrivateKey(keyTag, rolloverDays);\n\n            SaveZoneFile(primaryZone.Name);\n\n            return privateKey;\n        }\n\n        public void DeletePrimaryZoneDnssecPrivateKey(string zoneName, ushort keyTag)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.DeletePrivateKey(keyTag);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(string zoneName)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.PublishAllGeneratedKeys();\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void RolloverPrimaryZoneDnsKey(string zoneName, ushort keyTag)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            primaryZone.RolloverDnsKey(keyTag);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public async Task RetirePrimaryZoneDnsKeyAsync(string zoneName, ushort keyTag)\n        {\n            if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal)\n                throw new DnsServerException(\"No such primary zone was found: \" + zoneName);\n\n            await primaryZone.RetireDnsKeyAsync(keyTag);\n\n            SaveZoneFile(primaryZone.Name);\n        }\n\n        public void LoadTrustAnchorsTo(DnsClient dnsClient, string domain, DnsResourceRecordType type)\n        {\n            if (type == DnsResourceRecordType.DS)\n            {\n                domain = GetParentZone(domain);\n                if (domain is null)\n                    domain = \"\";\n            }\n\n            AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(domain, false);\n            if ((zoneInfo is not null) && (zoneInfo.ApexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned))\n            {\n                IReadOnlyList<DnsResourceRecord> dnsKeyRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.DNSKEY);\n                List<DnsResourceRecord> dsRecords = new List<DnsResourceRecord>(dnsKeyRecords.Count);\n\n                foreach (DnsResourceRecord dnsKeyRecord in dnsKeyRecords)\n                {\n                    DnsDNSKEYRecordData dnsKey = dnsKeyRecord.RDATA as DnsDNSKEYRecordData;\n\n                    if (dnsKey.Flags.HasFlag(DnsDnsKeyFlag.SecureEntryPoint) && !dnsKey.Flags.HasFlag(DnsDnsKeyFlag.Revoke))\n                        dsRecords.Add(new DnsResourceRecord(dnsKeyRecord.Name, DnsResourceRecordType.DS, DnsClass.IN, 0, dnsKey.CreateDS(dnsKeyRecord.Name, DnssecDigestType.SHA256)));\n                }\n\n                //set trust anchor\n                dnsClient.TrustAnchors[zoneInfo.Name] = dsRecords;\n            }\n        }\n\n        #endregion\n\n        #region zone listing\n\n        public IEnumerable<AuthZoneInfo> EnumerateAllZones()\n        {\n            _zoneIndexLock.EnterReadLock();\n            try\n            {\n                foreach (AuthZoneInfo zoneInfo in _zoneIndex)\n                    yield return zoneInfo;\n            }\n            finally\n            {\n                _zoneIndexLock.ExitReadLock();\n            }\n        }\n\n        public IReadOnlyList<AuthZoneInfo> GetAllZones()\n        {\n            _zoneIndexLock.EnterReadLock();\n            try\n            {\n                return _zoneIndex.ToArray();\n            }\n            finally\n            {\n                _zoneIndexLock.ExitReadLock();\n            }\n        }\n\n        public IReadOnlyList<AuthZoneInfo> GetZones(Func<AuthZoneInfo, bool> predicate)\n        {\n            _zoneIndexLock.EnterReadLock();\n            try\n            {\n                List<AuthZoneInfo> zoneInfoList = new List<AuthZoneInfo>();\n\n                foreach (AuthZoneInfo zoneInfo in _zoneIndex)\n                {\n                    if (predicate(zoneInfo))\n                        zoneInfoList.Add(zoneInfo);\n                }\n\n                return zoneInfoList;\n            }\n            finally\n            {\n                _zoneIndexLock.ExitReadLock();\n            }\n        }\n\n        public IReadOnlyList<AuthZoneInfo> GetAllCatalogZones()\n        {\n            _zoneIndexLock.EnterReadLock();\n            try\n            {\n                return _catalogZoneIndex.ToArray();\n            }\n            finally\n            {\n                _zoneIndexLock.ExitReadLock();\n            }\n        }\n\n        public IReadOnlyList<AuthZoneInfo> GetCatalogZones(Func<AuthZoneInfo, bool> predicate)\n        {\n            _zoneIndexLock.EnterReadLock();\n            try\n            {\n                List<AuthZoneInfo> catalogZoneInfoList = new List<AuthZoneInfo>();\n\n                foreach (AuthZoneInfo zone in _catalogZoneIndex)\n                {\n                    if (predicate(zone))\n                        catalogZoneInfoList.Add(zone);\n                }\n\n                return catalogZoneInfoList;\n            }\n            finally\n            {\n                _zoneIndexLock.ExitReadLock();\n            }\n        }\n\n        #endregion\n\n        #region zone record management\n\n        public void ListAllZoneRecords(string zoneName, List<DnsResourceRecord> records)\n        {\n            foreach (AuthZone authZone in _root.GetApexZoneWithSubDomainZones(zoneName))\n                authZone.ListAllRecords(records);\n        }\n\n        public void ListAllZoneRecords(string zoneName, DnsResourceRecordType[] types, List<DnsResourceRecord> records)\n        {\n            foreach (AuthZone authZone in _root.GetApexZoneWithSubDomainZones(zoneName))\n            {\n                foreach (DnsResourceRecordType type in types)\n                    records.AddRange(authZone.GetRecords(type));\n            }\n        }\n\n        public void ListAllRecords(string zoneName, string domain, List<DnsResourceRecord> records)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, domain);\n\n            if (_root.TryGet(zoneName, domain, out AuthZone authZone))\n                authZone.ListAllRecords(records);\n        }\n\n        public IEnumerable<DnsResourceRecord> EnumerateAllRecords(string zoneName, string domain, bool includeAllSubDomainNames = false)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, domain);\n\n            if (includeAllSubDomainNames)\n            {\n                foreach (AuthZone authZone in _root.GetSubDomainZoneWithSubDomainZones(domain))\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in authZone.Entries)\n                    {\n                        foreach (DnsResourceRecord record in entry.Value)\n                            yield return record;\n                    }\n                }\n            }\n            else\n            {\n                if (_root.TryGet(zoneName, domain, out AuthZone authZone))\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in authZone.Entries)\n                    {\n                        foreach (DnsResourceRecord record in entry.Value)\n                            yield return record;\n                    }\n                }\n            }\n        }\n\n        public IReadOnlyList<DnsResourceRecord> GetRecords(string zoneName, string domain, DnsResourceRecordType type)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, domain);\n\n            if (_root.TryGet(zoneName, domain, out AuthZone authZone))\n                return authZone.GetRecords(type);\n\n            return Array.Empty<DnsResourceRecord>();\n        }\n\n        public IReadOnlyDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> GetEntriesFor(string zoneName, string domain)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, domain);\n\n            if (_root.TryGet(zoneName, domain, out AuthZone authZone))\n                return authZone.Entries;\n\n            return new Dictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>(1);\n        }\n\n        public void SetRecords(string zoneName, IReadOnlyList<DnsResourceRecord> records)\n        {\n            for (int i = 1; i < records.Count; i++)\n            {\n                if (!records[i].Name.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase))\n                    throw new InvalidOperationException();\n\n                if (records[i].Type != records[0].Type)\n                    throw new InvalidOperationException();\n\n                if (records[i].Class != records[0].Class)\n                    throw new InvalidOperationException();\n            }\n\n            AuthZone authZone = GetOrAddSubDomainZone(zoneName, records[0].Name);\n\n            authZone.SetRecords(records[0].Type, records);\n\n            if (authZone is SubDomainZone subDomainZone)\n                subDomainZone.AutoUpdateState();\n        }\n\n        public void SetRecord(string zoneName, DnsResourceRecord record)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, record.Name);\n\n            AuthZone authZone = GetOrAddSubDomainZone(zoneName, record.Name);\n\n            authZone.SetRecords(record.Type, new DnsResourceRecord[] { record });\n\n            if (authZone is SubDomainZone subDomainZone)\n                subDomainZone.AutoUpdateState();\n        }\n\n        public bool AddRecord(string zoneName, DnsResourceRecord record)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, record.Name);\n\n            AuthZone authZone = GetOrAddSubDomainZone(zoneName, record.Name);\n\n            if (authZone.AddRecord(record))\n            {\n                if (authZone is SubDomainZone subDomainZone)\n                    subDomainZone.AutoUpdateState();\n\n                return true;\n            }\n\n            return false;\n        }\n\n        public void UpdateRecord(string zoneName, DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, oldRecord.Name);\n            ValidateIfDomainBelongsToZone(zoneName, newRecord.Name);\n\n            if (oldRecord.Type != newRecord.Type)\n                throw new DnsServerException(\"Cannot update record: new record must be of same type.\");\n\n            if (oldRecord.Type == DnsResourceRecordType.SOA)\n                throw new DnsServerException(\"Cannot update record: use SetRecords() for updating SOA record.\");\n\n            if (!_root.TryGet(zoneName, oldRecord.Name, out AuthZone authZone))\n                throw new DnsServerException(\"Cannot update record: zone '\" + zoneName + \"' does not exists.\");\n\n            switch (oldRecord.Type)\n            {\n                case DnsResourceRecordType.CNAME:\n                case DnsResourceRecordType.DNAME:\n                case DnsResourceRecordType.APP:\n                    if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase))\n                    {\n                        authZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord });\n\n                        if (authZone is SubDomainZone subDomainZone)\n                            subDomainZone.AutoUpdateState();\n                    }\n                    else\n                    {\n                        authZone.DeleteRecords(oldRecord.Type);\n\n                        if (authZone is SubDomainZone subDomainZone)\n                        {\n                            if (authZone.IsEmpty)\n                                _root.TryRemove(oldRecord.Name, out SubDomainZone _); //remove empty sub zone\n                            else\n                                subDomainZone.AutoUpdateState();\n                        }\n\n                        AuthZone newZone = GetOrAddSubDomainZone(zoneName, newRecord.Name);\n\n                        newZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord });\n\n                        if (newZone is SubDomainZone subDomainZone1)\n                            subDomainZone1.AutoUpdateState();\n                    }\n                    break;\n\n                default:\n                    if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase))\n                    {\n                        authZone.UpdateRecord(oldRecord, newRecord);\n\n                        if (authZone is SubDomainZone subDomainZone)\n                            subDomainZone.AutoUpdateState();\n                    }\n                    else\n                    {\n                        if (!authZone.DeleteRecord(oldRecord.Type, oldRecord.RDATA))\n                            throw new DnsWebServiceException(\"Cannot update record: the old record does not exists.\");\n\n                        if (authZone is SubDomainZone subDomainZone)\n                        {\n                            if (authZone.IsEmpty)\n                                _root.TryRemove(oldRecord.Name, out SubDomainZone _); //remove empty sub zone\n                            else\n                                subDomainZone.AutoUpdateState();\n                        }\n\n                        AuthZone newZone = GetOrAddSubDomainZone(zoneName, newRecord.Name);\n\n                        newZone.AddRecord(newRecord);\n\n                        if (newZone is SubDomainZone subDomainZone1)\n                            subDomainZone1.AutoUpdateState();\n                    }\n                    break;\n            }\n        }\n\n        public bool DeleteRecord(string zoneName, DnsResourceRecord record)\n        {\n            return DeleteRecord(zoneName, record.Name, record.Type, record.RDATA);\n        }\n\n        public bool DeleteRecord(string zoneName, string domain, DnsResourceRecordType type, DnsResourceRecordData rdata)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, domain);\n\n            if (_root.TryGet(zoneName, domain, out AuthZone authZone))\n            {\n                if (authZone.DeleteRecord(type, rdata))\n                {\n                    if (authZone is SubDomainZone subDomainZone)\n                    {\n                        if (authZone.IsEmpty)\n                            _root.TryRemove(domain, out SubDomainZone _); //remove empty sub zone\n                        else\n                            subDomainZone.AutoUpdateState();\n                    }\n\n                    return true;\n                }\n            }\n\n            return false;\n        }\n\n        public bool DeleteRecords(string zoneName, string domain, DnsResourceRecordType type)\n        {\n            ValidateIfDomainBelongsToZone(zoneName, domain);\n\n            if (_root.TryGet(zoneName, domain, out AuthZone authZone))\n            {\n                if (authZone.DeleteRecords(type))\n                {\n                    if (authZone is SubDomainZone subDomainZone)\n                    {\n                        if (authZone.IsEmpty)\n                            _root.TryRemove(domain, out SubDomainZone _); //remove empty sub zone\n                        else\n                            subDomainZone.AutoUpdateState();\n                    }\n\n                    return true;\n                }\n            }\n\n            return false;\n        }\n\n        #endregion\n\n        #region zone transfer / import\n\n        public IReadOnlyList<DnsResourceRecord> QueryZoneTransferRecords(string zoneName)\n        {\n            AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName);\n            if (zoneInfo is null)\n                throw new InvalidOperationException(\"Zone was not found: \" + zoneName);\n\n            //primary, secondary, and forwarder zones support zone transfer\n            IReadOnlyList<DnsResourceRecord> soaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA);\n            if (soaRecords.Count != 1)\n                throw new InvalidOperationException(\"Zone must be a primary, secondary, or forwarder zone.\");\n\n            DnsResourceRecord soaRecord = soaRecords[0];\n\n            List<DnsResourceRecord> records = new List<DnsResourceRecord>();\n            ListAllZoneRecords(zoneName, records);\n\n            List<DnsResourceRecord> xfrRecords = new List<DnsResourceRecord>(records.Count + 1);\n\n            //start message\n            xfrRecords.Add(soaRecord);\n\n            foreach (DnsResourceRecord record in records)\n            {\n                GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo();\n                if (authRecordInfo.Disabled)\n                    continue;\n\n                switch (record.Type)\n                {\n                    case DnsResourceRecordType.SOA:\n                        break; //skip record\n\n                    case DnsResourceRecordType.NS:\n                        xfrRecords.Add(record);\n\n                        IReadOnlyList<DnsResourceRecord> glueRecords = (authRecordInfo as NSRecordInfo).GlueRecords;\n                        if (glueRecords is not null)\n                        {\n                            foreach (DnsResourceRecord glueRecord in glueRecords)\n                                xfrRecords.Add(glueRecord);\n                        }\n                        break;\n\n                    default:\n                        xfrRecords.Add(record);\n                        break;\n                }\n            }\n\n            //end message\n            xfrRecords.Add(soaRecord);\n\n            return xfrRecords;\n        }\n\n        public IReadOnlyList<DnsResourceRecord> QueryIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord clientSoaRecord)\n        {\n            AuthZoneInfo authZone = GetAuthZoneInfo(zoneName, true);\n            if (authZone is null)\n                throw new InvalidOperationException(\"Zone was not found: \" + zoneName);\n\n            //primary, secondary, forwarder, and catalog zones support zone transfer\n            IReadOnlyList<DnsResourceRecord> soaRecords = authZone.ApexZone.GetRecords(DnsResourceRecordType.SOA);\n            if (soaRecords.Count != 1)\n                throw new InvalidOperationException(\"No SOA record was found for IXFR.\");\n\n            DnsResourceRecord currentSoaRecord = soaRecords[0];\n            uint clientSerial = (clientSoaRecord.RDATA as DnsSOARecordData).Serial;\n\n            if (clientSerial == (currentSoaRecord.RDATA as DnsSOARecordData).Serial)\n            {\n                //zone not modified\n                return [currentSoaRecord];\n            }\n\n            //find history record start from client serial\n            IReadOnlyList<DnsResourceRecord> zoneHistory = authZone.ZoneHistory;\n\n            int index = 0;\n            while (index < zoneHistory.Count)\n            {\n                //check difference sequence\n                if ((zoneHistory[index].RDATA as DnsSOARecordData).Serial == clientSerial)\n                    break; //found history for client's serial\n\n                //skip to next difference sequence\n                index++;\n                int soaCount = 1;\n\n                while (index < zoneHistory.Count)\n                {\n                    if (zoneHistory[index].Type == DnsResourceRecordType.SOA)\n                    {\n                        soaCount++;\n\n                        if (soaCount == 3)\n                            break;\n                    }\n\n                    index++;\n                }\n            }\n\n            if (index == zoneHistory.Count)\n            {\n                //client's serial was not found in zone history\n                //do full zone transfer\n                return QueryZoneTransferRecords(zoneName);\n            }\n\n            List<DnsResourceRecord> xfrRecords = new List<DnsResourceRecord>();\n\n            //start incremental message\n            xfrRecords.Add(currentSoaRecord);\n\n            //write history\n            for (int i = index; i < zoneHistory.Count; i++)\n                xfrRecords.Add(zoneHistory[i]);\n\n            //end incremental message\n            xfrRecords.Add(currentSoaRecord);\n\n            //condense\n            return CondenseIncrementalZoneTransferRecords(zoneName, clientSoaRecord, xfrRecords);\n        }\n\n        public void SyncZoneTransferRecords(string zoneName, IReadOnlyList<DnsResourceRecord> xfrRecords)\n        {\n            if ((xfrRecords.Count < 2) || (xfrRecords[0].Type != DnsResourceRecordType.SOA) || !xfrRecords[0].Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || !xfrRecords[xfrRecords.Count - 1].Equals(xfrRecords[0]))\n                throw new DnsServerException(\"Invalid AXFR response was received.\");\n\n            List<DnsResourceRecord> latestRecords = new List<DnsResourceRecord>(xfrRecords.Count);\n            List<DnsResourceRecord> allGlueRecords = new List<DnsResourceRecord>(4);\n\n            if (zoneName.Length == 0)\n            {\n                //root zone case\n                for (int i = 1; i < xfrRecords.Count; i++)\n                {\n                    DnsResourceRecord record = xfrRecords[i];\n\n                    switch (record.Type)\n                    {\n                        case DnsResourceRecordType.A:\n                        case DnsResourceRecordType.AAAA:\n                            if (!allGlueRecords.Contains(record))\n                                allGlueRecords.Add(record);\n\n                            break;\n\n                        default:\n                            if (!latestRecords.Contains(record))\n                                latestRecords.Add(record);\n\n                            break;\n                    }\n                }\n            }\n            else\n            {\n                for (int i = 1; i < xfrRecords.Count; i++)\n                {\n                    DnsResourceRecord record = xfrRecords[i];\n\n                    if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith(\".\" + zoneName, StringComparison.OrdinalIgnoreCase))\n                    {\n                        if (!latestRecords.Contains(record))\n                            latestRecords.Add(record);\n                    }\n                    else if (!allGlueRecords.Contains(record))\n                    {\n                        allGlueRecords.Add(record);\n                    }\n                }\n            }\n\n            if (allGlueRecords.Count > 0)\n            {\n                foreach (DnsResourceRecord record in latestRecords)\n                {\n                    if (record.Type == DnsResourceRecordType.NS)\n                        record.SyncGlueRecords(allGlueRecords);\n                }\n            }\n\n            //sync records\n            List<DnsResourceRecord> currentRecords = new List<DnsResourceRecord>();\n            ListAllZoneRecords(zoneName, currentRecords);\n\n            Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> currentRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(currentRecords);\n            Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> latestRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(latestRecords);\n\n            //remove domains that do not exists in new records\n            foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> currentDomain in currentRecordsGroupedByDomain)\n            {\n                if (!latestRecordsGroupedByDomain.ContainsKey(currentDomain.Key))\n                    _root.TryRemove(currentDomain.Key, out SubDomainZone _);\n            }\n\n            //sync new records\n            foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> latestEntries in latestRecordsGroupedByDomain)\n            {\n                AuthZone zone = GetOrAddSubDomainZone(zoneName, latestEntries.Key);\n\n                if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    zone.SyncRecords(latestEntries.Value);\n                else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    zone.SyncRecords(latestEntries.Value);\n            }\n\n            if (!_root.TryGet(zoneName, out ApexZone apexZone))\n                throw new InvalidOperationException();\n\n            apexZone.UpdateDnssecStatus();\n\n            SaveZoneFile(apexZone.Name);\n        }\n\n        public IReadOnlyList<DnsResourceRecord> SyncIncrementalZoneTransferRecords(string zoneName, IReadOnlyList<DnsResourceRecord> xfrRecords)\n        {\n            if ((xfrRecords.Count < 2) || (xfrRecords[0].Type != DnsResourceRecordType.SOA) || !xfrRecords[0].Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || !xfrRecords[xfrRecords.Count - 1].Equals(xfrRecords[0]))\n                throw new DnsServerException(\"Invalid IXFR/AXFR response was received.\");\n\n            if ((xfrRecords.Count < 4) || (xfrRecords[1].Type != DnsResourceRecordType.SOA))\n            {\n                //received AXFR response\n                SyncZoneTransferRecords(zoneName, xfrRecords);\n                return Array.Empty<DnsResourceRecord>();\n            }\n\n            if (!_root.TryGet(zoneName, out ApexZone apexZone))\n                throw new InvalidOperationException(\"No such zone was found: \" + zoneName);\n\n            IReadOnlyList<DnsResourceRecord> soaRecords = apexZone.GetRecords(DnsResourceRecordType.SOA);\n            if (soaRecords.Count != 1)\n                throw new InvalidOperationException(\"No authoritative zone was found: \" + zoneName);\n\n            //process IXFR response\n            DnsResourceRecord currentSoaRecord = soaRecords[0];\n            DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;\n\n            List<DnsResourceRecord> condensedXfrRecords = CondenseIncrementalZoneTransferRecords(zoneName, currentSoaRecord, xfrRecords);\n\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedGlueRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> addedGlueRecords = new List<DnsResourceRecord>();\n\n            //read and apply difference sequences\n            int index = 1;\n            int count = condensedXfrRecords.Count - 1;\n\n            while (index < count)\n            {\n                //read deleted records\n                DnsResourceRecord deletedSoaRecord = condensedXfrRecords[index];\n                if ((deletedSoaRecord.Type != DnsResourceRecordType.SOA) || !deletedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    throw new InvalidOperationException();\n\n                index++;\n\n                while (index < count)\n                {\n                    DnsResourceRecord record = condensedXfrRecords[index];\n                    if (record.Type == DnsResourceRecordType.SOA)\n                        break;\n\n                    if (zoneName.Length == 0)\n                    {\n                        //root zone case\n                        switch (record.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                            case DnsResourceRecordType.AAAA:\n                                deletedGlueRecords.Add(record);\n                                break;\n\n                            default:\n                                deletedRecords.Add(record);\n                                break;\n                        }\n                    }\n                    else\n                    {\n                        if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith(\".\" + zoneName, StringComparison.OrdinalIgnoreCase))\n                        {\n                            deletedRecords.Add(record);\n                        }\n                        else\n                        {\n                            switch (record.Type)\n                            {\n                                case DnsResourceRecordType.A:\n                                case DnsResourceRecordType.AAAA:\n                                    deletedGlueRecords.Add(record);\n                                    break;\n                            }\n                        }\n                    }\n\n                    index++;\n                }\n\n                //read added records\n                DnsResourceRecord addedSoaRecord = condensedXfrRecords[index];\n                if (!addedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    throw new InvalidOperationException();\n\n                index++;\n\n                while (index < count)\n                {\n                    DnsResourceRecord record = condensedXfrRecords[index];\n                    if (record.Type == DnsResourceRecordType.SOA)\n                        break;\n\n                    if (zoneName.Length == 0)\n                    {\n                        //root zone case\n                        switch (record.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                            case DnsResourceRecordType.AAAA:\n                                addedGlueRecords.Add(record);\n                                break;\n\n                            default:\n                                addedRecords.Add(record);\n                                break;\n                        }\n                    }\n                    else\n                    {\n                        if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith(\".\" + zoneName, StringComparison.OrdinalIgnoreCase))\n                        {\n                            addedRecords.Add(record);\n                        }\n                        else\n                        {\n                            switch (record.Type)\n                            {\n                                case DnsResourceRecordType.A:\n                                case DnsResourceRecordType.AAAA:\n                                    addedGlueRecords.Add(record);\n                                    break;\n                            }\n                        }\n                    }\n\n                    index++;\n                }\n\n                //check sequence soa serial\n                DnsSOARecordData deletedSoa = deletedSoaRecord.RDATA as DnsSOARecordData;\n\n                if (currentSoa.Serial != deletedSoa.Serial)\n                    throw new InvalidOperationException(\"Current SOA serial does not match with the IXFR difference sequence deleted SOA.\");\n\n                //sync difference sequence\n                if (deletedRecords.Count > 0)\n                {\n                    foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> deletedEntry in DnsResourceRecord.GroupRecords(deletedRecords))\n                    {\n                        AuthZone zone = GetOrAddSubDomainZone(zoneName, deletedEntry.Key);\n\n                        if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                        {\n                            zone.SyncRecords(deletedEntry.Value, null);\n                        }\n                        else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                        {\n                            zone.SyncRecords(deletedEntry.Value, null);\n\n                            if (zone.IsEmpty)\n                                _root.TryRemove(deletedEntry.Key, out SubDomainZone _); //remove empty sub zone\n                        }\n                    }\n                }\n\n                if (addedRecords.Count > 0)\n                {\n                    foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> addedEntry in DnsResourceRecord.GroupRecords(addedRecords))\n                    {\n                        AuthZone zone = GetOrAddSubDomainZone(zoneName, addedEntry.Key);\n\n                        if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                            zone.SyncRecords(null, addedEntry.Value);\n                        else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                            zone.SyncRecords(null, addedEntry.Value);\n                    }\n                }\n\n                if ((deletedGlueRecords.Count > 0) || (addedGlueRecords.Count > 0))\n                {\n                    foreach (AuthZone zone in _root.GetApexZoneWithSubDomainZones(zoneName))\n                        zone.SyncGlueRecords(deletedGlueRecords, addedGlueRecords);\n                }\n\n                {\n                    AuthZone zone = GetOrAddSubDomainZone(zoneName, zoneName);\n\n                    addedSoaRecord.CopyRecordInfoFrom(currentSoaRecord);\n\n                    zone.LoadRecords(DnsResourceRecordType.SOA, new DnsResourceRecord[] { addedSoaRecord });\n                }\n\n                //check next difference sequence\n                currentSoa = addedSoaRecord.RDATA as DnsSOARecordData;\n\n                deletedRecords.Clear();\n                deletedGlueRecords.Clear();\n                addedRecords.Clear();\n                addedGlueRecords.Clear();\n            }\n\n            apexZone.UpdateDnssecStatus();\n\n            SaveZoneFile(apexZone.Name);\n\n            //return history\n            List<DnsResourceRecord> historyRecords = new List<DnsResourceRecord>(xfrRecords.Count - 2);\n\n            for (int i = 1; i < xfrRecords.Count - 1; i++)\n                historyRecords.Add(xfrRecords[i]);\n\n            return historyRecords;\n        }\n\n        private static List<DnsResourceRecord> CondenseIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord currentSoaRecord, IReadOnlyList<DnsResourceRecord> xfrRecords)\n        {\n            DnsResourceRecord firstSoaRecord = xfrRecords[0];\n            DnsResourceRecord lastSoaRecord = xfrRecords[xfrRecords.Count - 1];\n\n            DnsResourceRecord firstDeletedSoaRecord = null;\n            DnsResourceRecord lastAddedSoaRecord = null;\n\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedGlueRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> addedGlueRecords = new List<DnsResourceRecord>();\n\n            //read and apply difference sequences\n            int index = 1;\n            int count = xfrRecords.Count - 1;\n            DnsSOARecordData currentSoa = (DnsSOARecordData)currentSoaRecord.RDATA;\n\n            while (index < count)\n            {\n                //read deleted records\n                DnsResourceRecord deletedSoaRecord = xfrRecords[index];\n                if ((deletedSoaRecord.Type != DnsResourceRecordType.SOA) || !deletedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    throw new InvalidOperationException();\n\n                if (firstDeletedSoaRecord is null)\n                    firstDeletedSoaRecord = deletedSoaRecord;\n\n                index++;\n\n                while (index < count)\n                {\n                    DnsResourceRecord record = xfrRecords[index];\n                    if (record.Type == DnsResourceRecordType.SOA)\n                        break;\n\n                    if (zoneName.Length == 0)\n                    {\n                        //root zone case\n                        switch (record.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                            case DnsResourceRecordType.AAAA:\n                                if (!addedGlueRecords.Remove(record))\n                                    deletedGlueRecords.Add(record);\n\n                                break;\n\n                            default:\n                                if (!addedRecords.Remove(record))\n                                    deletedRecords.Add(record);\n\n                                break;\n                        }\n                    }\n                    else\n                    {\n                        if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith(\".\" + zoneName, StringComparison.OrdinalIgnoreCase))\n                        {\n                            if (!addedRecords.Remove(record))\n                                deletedRecords.Add(record);\n                        }\n                        else\n                        {\n                            switch (record.Type)\n                            {\n                                case DnsResourceRecordType.A:\n                                case DnsResourceRecordType.AAAA:\n                                    if (!addedGlueRecords.Remove(record))\n                                        deletedGlueRecords.Add(record);\n\n                                    break;\n                            }\n                        }\n                    }\n\n                    index++;\n                }\n\n                //read added records\n                DnsResourceRecord addedSoaRecord = xfrRecords[index];\n                if (!addedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                    throw new InvalidOperationException();\n\n                lastAddedSoaRecord = addedSoaRecord;\n\n                index++;\n\n                while (index < count)\n                {\n                    DnsResourceRecord record = xfrRecords[index];\n                    if (record.Type == DnsResourceRecordType.SOA)\n                        break;\n\n                    if (zoneName.Length == 0)\n                    {\n                        //root zone case\n                        switch (record.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                            case DnsResourceRecordType.AAAA:\n                                if (!deletedGlueRecords.Remove(record))\n                                    addedGlueRecords.Add(record);\n\n                                break;\n\n                            default:\n                                if (!deletedRecords.Remove(record))\n                                    addedRecords.Add(record);\n\n                                break;\n                        }\n                    }\n                    else\n                    {\n                        if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith(\".\" + zoneName, StringComparison.OrdinalIgnoreCase))\n                        {\n                            if (!deletedRecords.Remove(record))\n                                addedRecords.Add(record);\n                        }\n                        else\n                        {\n                            switch (record.Type)\n                            {\n                                case DnsResourceRecordType.A:\n                                case DnsResourceRecordType.AAAA:\n                                    if (!deletedGlueRecords.Remove(record))\n                                        addedGlueRecords.Add(record);\n\n                                    break;\n                            }\n                        }\n                    }\n\n                    index++;\n                }\n\n                //check sequence soa serial\n                DnsSOARecordData deletedSoa = deletedSoaRecord.RDATA as DnsSOARecordData;\n\n                if (currentSoa.Serial != deletedSoa.Serial)\n                    throw new InvalidOperationException(\"Current SOA serial does not match with the IXFR difference sequence deleted SOA.\");\n\n                //check next difference sequence\n                currentSoa = addedSoaRecord.RDATA as DnsSOARecordData;\n            }\n\n            //create condensed records\n            List<DnsResourceRecord> condensedRecords = new List<DnsResourceRecord>(2 + 2 + deletedRecords.Count + deletedGlueRecords.Count + addedRecords.Count + addedGlueRecords.Count);\n\n            condensedRecords.Add(firstSoaRecord);\n\n            condensedRecords.Add(firstDeletedSoaRecord);\n            condensedRecords.AddRange(deletedRecords);\n            condensedRecords.AddRange(deletedGlueRecords);\n\n            condensedRecords.Add(lastAddedSoaRecord);\n            condensedRecords.AddRange(addedRecords);\n            condensedRecords.AddRange(addedGlueRecords);\n\n            condensedRecords.Add(lastSoaRecord);\n\n            return condensedRecords;\n        }\n\n        internal void ImportRecords(string zoneName, IReadOnlyList<DnsResourceRecord> records, bool overwrite, bool overwriteSoaSerial)\n        {\n            _ = _root.FindZone(zoneName, out _, out _, out ApexZone apexZone, out _);\n            if ((apexZone is null) || !apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n                throw new DnsServerException(\"No such zone was found: \" + zoneName);\n\n            if ((apexZone is not PrimaryZone) && (apexZone is not ForwarderZone))\n                throw new DnsServerException(\"Zone must be a primary or forwarder type: \" + apexZone.ToString());\n\n            List<DnsResourceRecord> soaRRSet = null;\n\n            foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> zoneEntry in DnsResourceRecord.GroupRecords(records))\n            {\n                if (zoneName.Equals(zoneEntry.Key, StringComparison.OrdinalIgnoreCase))\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> rrsetEntry in zoneEntry.Value)\n                    {\n                        switch (rrsetEntry.Key)\n                        {\n                            case DnsResourceRecordType.CNAME:\n                            case DnsResourceRecordType.DNAME:\n                                apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value);\n                                break;\n\n                            case DnsResourceRecordType.SOA:\n                                if (!overwriteSoaSerial)\n                                    rrsetEntry.Value[0].GetAuthSOARecordInfo().UseSoaSerialDateScheme = apexZone.GetRecords(DnsResourceRecordType.SOA)[0].GetAuthSOARecordInfo().UseSoaSerialDateScheme;\n\n                                apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value);\n                                soaRRSet = rrsetEntry.Value;\n                                break;\n\n                            default:\n                                if (overwrite)\n                                {\n                                    apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value);\n                                }\n                                else\n                                {\n                                    foreach (DnsResourceRecord record in rrsetEntry.Value)\n                                        apexZone.AddRecord(record);\n                                }\n                                break;\n                        }\n                    }\n                }\n                else\n                {\n                    ValidateIfDomainBelongsToZone(zoneName, zoneEntry.Key);\n\n                    AuthZone authZone = GetOrAddSubDomainZone(zoneName, zoneEntry.Key);\n\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> rrsetEntry in zoneEntry.Value)\n                    {\n                        switch (rrsetEntry.Key)\n                        {\n                            case DnsResourceRecordType.CNAME:\n                            case DnsResourceRecordType.DNAME:\n                            case DnsResourceRecordType.SOA:\n                                authZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value);\n                                break;\n\n                            default:\n                                if (overwrite)\n                                {\n                                    authZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value);\n                                }\n                                else\n                                {\n                                    foreach (DnsResourceRecord record in rrsetEntry.Value)\n                                        authZone.AddRecord(record);\n                                }\n                                break;\n                        }\n                    }\n\n                    if (authZone is SubDomainZone subDomainZone)\n                        subDomainZone.AutoUpdateState();\n                }\n            }\n\n            if (overwriteSoaSerial && (soaRRSet is not null) && ((apexZone is PrimaryZone) || (apexZone is ForwarderZone)))\n                apexZone.SetSoaSerial((soaRRSet[0].RDATA as DnsSOARecordData).Serial);\n\n            SaveZoneFile(apexZone.Name);\n        }\n\n        #endregion\n\n        #region query processing\n\n        public DnsDatagram QueryClosestDelegation(DnsDatagram request)\n        {\n            _ = _root.FindZone(request.Question[0].Name, out _, out SubDomainZone delegation, out ApexZone apexZone, out _);\n            if (delegation is not null)\n            {\n                bool dnssecOk = request.DnssecOk && (apexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned);\n\n                return GetReferralResponse(request, dnssecOk, delegation, apexZone);\n            }\n\n            if (apexZone is StubZone)\n                return GetReferralResponse(request, false, apexZone, apexZone);\n\n            //no delegation found\n            return null;\n        }\n\n        public async Task<DnsDatagram> QueryAsync(DnsDatagram request, IPAddress remoteIP, bool isRecursionAllowed)\n        {\n            AuthZone zone = _root.FindZone(request.Question[0].Name, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone apexZone, out bool hasSubDomains);\n\n            if ((apexZone is null) || !apexZone.IsActive)\n                return null; //no authority for requested zone\n\n            if (!await IsQueryAllowedAsync(apexZone, remoteIP))\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.Refused, request.Question);\n\n            return InternalQuery(request, isRecursionAllowed, zone, closest, delegation, apexZone, hasSubDomains);\n        }\n\n        public DnsDatagram Query(DnsDatagram request, bool isRecursionAllowed)\n        {\n            AuthZone zone = _root.FindZone(request.Question[0].Name, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone apexZone, out bool hasSubDomains);\n\n            if ((apexZone is null) || !apexZone.IsActive)\n                return null; //no authority for requested zone\n\n            return InternalQuery(request, isRecursionAllowed, zone, closest, delegation, apexZone, hasSubDomains);\n        }\n\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        private DnsDatagram InternalQuery(DnsDatagram request, bool isRecursionAllowed, AuthZone zone, SubDomainZone closest, SubDomainZone delegation, ApexZone apexZone, bool hasSubDomains)\n        {\n            DnsQuestionRecord question = request.Question[0];\n            bool dnssecOk = request.DnssecOk && (apexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned);\n\n            if ((zone is null) || !zone.IsActive)\n            {\n                //zone not found\n                if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length))\n                    return GetReferralResponse(request, dnssecOk, delegation, apexZone);\n\n                if (apexZone is StubZone)\n                    return GetReferralResponse(request, false, apexZone, apexZone);\n\n                DnsResponseCode rCode = DnsResponseCode.NoError;\n                IReadOnlyList<DnsResourceRecord> answer = null;\n                IReadOnlyList<DnsResourceRecord> authority = null;\n\n                if (closest is not null)\n                {\n                    answer = closest.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk);\n                    if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME))\n                    {\n                        if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer))\n                            rCode = DnsResponseCode.YXDomain;\n                    }\n                    else\n                    {\n                        answer = null;\n                        authority = closest.QueryRecords(DnsResourceRecordType.APP, false);\n                    }\n                }\n\n                if (((answer is null) || (answer.Count == 0)) && ((authority is null) || (authority.Count == 0)))\n                {\n                    answer = apexZone.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk);\n                    if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME))\n                    {\n                        if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer))\n                            rCode = DnsResponseCode.YXDomain;\n                    }\n                    else\n                    {\n                        answer = null;\n                        authority = apexZone.QueryRecords(DnsResourceRecordType.APP, false);\n                        if (authority.Count == 0)\n                        {\n                            if ((apexZone is ForwarderZone) || (apexZone is SecondaryForwarderZone))\n                                return GetForwarderResponse(request, null, closest, apexZone); //no DNAME or APP record available so process FWD response\n\n                            if (!hasSubDomains)\n                                rCode = DnsResponseCode.NxDomain;\n\n                            authority = apexZone.QueryRecords(DnsResourceRecordType.SOA, dnssecOk);\n\n                            if (dnssecOk)\n                            {\n                                //add proof of non existence (NXDOMAIN) to prove the qname does not exists\n                                IReadOnlyList<DnsResourceRecord> nsecRecords;\n\n                                if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3)\n                                    nsecRecords = _root.FindNSec3ProofOfNonExistenceNxDomain(question.Name, false);\n                                else\n                                    nsecRecords = _root.FindNSecProofOfNonExistenceNxDomain(question.Name, false);\n\n                                if (nsecRecords.Count > 0)\n                                {\n                                    List<DnsResourceRecord> newAuthority = new List<DnsResourceRecord>(authority.Count + nsecRecords.Count);\n\n                                    newAuthority.AddRange(authority);\n                                    newAuthority.AddRange(nsecRecords);\n\n                                    authority = newAuthority;\n                                }\n                            }\n                        }\n                    }\n                }\n\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rCode, request.Question, answer, authority);\n            }\n            else\n            {\n                //zone found\n                if (question.Type == DnsResourceRecordType.DS)\n                {\n                    if (zone is ApexZone)\n                    {\n                        if ((delegation is null) || !delegation.IsActive || !delegation.AuthoritativeZone.IsActive || (delegation.Name.Length > apexZone.Name.Length))\n                            return null; //no authoritative parent side delegation zone available to answer for DS\n\n                        zone = delegation; //switch zone to parent side sub domain delegation zone for DS record\n\n                        if (request.DnssecOk)\n                            dnssecOk = delegation.AuthoritativeZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned;\n                    }\n                }\n                else if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length))\n                {\n                    //zone is delegation\n                    return GetReferralResponse(request, dnssecOk, delegation, apexZone);\n                }\n\n                DnsResponseCode rCode = DnsResponseCode.NoError;\n                IReadOnlyList<DnsResourceRecord> answer = null;\n                IReadOnlyList<DnsResourceRecord> authority = null;\n                IReadOnlyList<DnsResourceRecord> additional = null;\n\n                if (closest is not null)\n                {\n                    answer = closest.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk);\n                    if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME))\n                    {\n                        if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer))\n                            rCode = DnsResponseCode.YXDomain;\n                    }\n                }\n\n                if (((answer is null) || (answer.Count == 0)) && (question.Name.Length > apexZone.Name.Length))\n                {\n                    //query for DNAME only for subdomain names\n                    answer = apexZone.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk);\n                    if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME))\n                    {\n                        if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer))\n                            rCode = DnsResponseCode.YXDomain;\n                    }\n                }\n\n                if ((answer is null) || (answer.Count == 0))\n                {\n                    answer = zone.QueryRecords(question.Type, dnssecOk);\n                    if (answer.Count == 0)\n                    {\n                        //record type not found\n                        if (question.Type == DnsResourceRecordType.DS)\n                        {\n                            //check for correct auth zone\n                            if (apexZone.Name.Equals(question.Name, StringComparison.OrdinalIgnoreCase))\n                            {\n                                //current auth zone is child side; find parent side auth zone for DS\n                                string parentZone = GetParentZone(question.Name);\n                                if (parentZone is null)\n                                    parentZone = string.Empty;\n\n                                _ = _root.FindZone(parentZone, out _, out _, out apexZone, out _);\n\n                                if ((apexZone is null) || !apexZone.IsActive)\n                                    return null; //no authority for requested zone\n                            }\n                        }\n                        else\n                        {\n                            //check for delegation, stub & forwarder\n                            if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length))\n                                return GetReferralResponse(request, dnssecOk, delegation, apexZone);\n\n                            if (apexZone is StubZone)\n                                return GetReferralResponse(request, false, apexZone, apexZone);\n                        }\n\n                        authority = zone.QueryRecords(DnsResourceRecordType.APP, false);\n                        if (authority.Count == 0)\n                        {\n                            if ((apexZone is ForwarderZone) || (apexZone is SecondaryForwarderZone))\n                                return GetForwarderResponse(request, zone, closest, apexZone); //no APP record available so process FWD response\n\n                            authority = apexZone.QueryRecords(DnsResourceRecordType.SOA, dnssecOk);\n\n                            if (dnssecOk)\n                            {\n                                //add proof of non existence (NODATA) to prove that no such type or record exists\n                                IReadOnlyList<DnsResourceRecord> nsecRecords;\n\n                                if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3)\n                                    nsecRecords = _root.FindNSec3ProofOfNonExistenceNoData(question.Name, zone, apexZone);\n                                else\n                                    nsecRecords = _root.FindNSecProofOfNonExistenceNoData(question.Name, zone);\n\n                                if (nsecRecords.Count > 0)\n                                {\n                                    List<DnsResourceRecord> newAuthority = new List<DnsResourceRecord>(authority.Count + nsecRecords.Count);\n\n                                    newAuthority.AddRange(authority);\n                                    newAuthority.AddRange(nsecRecords);\n\n                                    authority = newAuthority;\n                                }\n                            }\n                        }\n\n                        additional = null;\n                    }\n                    else\n                    {\n                        //record type found\n                        if (zone.Name.StartsWith('*') && !zone.Name.Equals(question.Name, StringComparison.OrdinalIgnoreCase))\n                        {\n                            //wildcard zone; generate new answer records\n                            DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answer.Count];\n\n                            for (int i = 0; i < answer.Count; i++)\n                                wildcardAnswers[i] = new DnsResourceRecord(question.Name, answer[i].Type, answer[i].Class, answer[i].TTL, answer[i].RDATA) { Tag = answer[i].Tag };\n\n                            answer = wildcardAnswers;\n\n                            //add proof of non existence (WILDCARD) to prove that the wildcard expansion was legit and the qname actually does not exists\n                            if (dnssecOk)\n                            {\n                                IReadOnlyList<DnsResourceRecord> nsecRecords;\n\n                                if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3)\n                                    nsecRecords = _root.FindNSec3ProofOfNonExistenceNxDomain(question.Name, true);\n                                else\n                                    nsecRecords = _root.FindNSecProofOfNonExistenceNxDomain(question.Name, true);\n\n                                if (nsecRecords.Count > 0)\n                                    authority = nsecRecords;\n                            }\n                        }\n\n                        DnsResourceRecord lastRR = answer[answer.Count - 1];\n                        if ((lastRR.Type != question.Type) && (question.Type != DnsResourceRecordType.ANY))\n                        {\n                            switch (lastRR.Type)\n                            {\n                                case DnsResourceRecordType.CNAME:\n                                    List<DnsResourceRecord> newAnswers = new List<DnsResourceRecord>(answer.Count + 1);\n                                    newAnswers.AddRange(answer);\n\n                                    ResolveCNAME(question, dnssecOk, lastRR, newAnswers);\n\n                                    answer = newAnswers;\n                                    break;\n\n                                case DnsResourceRecordType.ANAME:\n                                case DnsResourceRecordType.ALIAS:\n                                    authority = apexZone.GetRecords(DnsResourceRecordType.SOA); //adding SOA for use with NO DATA response\n                                    break;\n                            }\n                        }\n\n                        switch (question.Type)\n                        {\n                            case DnsResourceRecordType.NS:\n                            case DnsResourceRecordType.MX:\n                            case DnsResourceRecordType.SRV:\n                            case DnsResourceRecordType.SVCB:\n                            case DnsResourceRecordType.HTTPS:\n                                additional = GetAdditionalRecords(answer, dnssecOk);\n                                break;\n\n                            default:\n                                additional = null;\n                                break;\n                        }\n                    }\n                }\n\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rCode, request.Question, answer, authority, additional);\n            }\n        }\n\n        private static async Task<bool> IsQueryAllowedAsync(ApexZone apexZone, IPAddress remoteIP)\n        {\n            async Task<bool> IsZoneNameServerAllowedAsync()\n            {\n                IReadOnlyList<NameServerAddress> zoneNameServers = await apexZone.GetAllResolvedNameServerAddressesAsync();\n\n                foreach (NameServerAddress nameServer in zoneNameServers)\n                {\n                    if (nameServer.IPEndPoint.Address.Equals(remoteIP))\n                        return true;\n                }\n\n                return false;\n            }\n\n            CatalogZone catalogZone = apexZone.CatalogZone;\n            if (catalogZone is not null)\n            {\n                if (!apexZone.OverrideCatalogQueryAccess)\n                    apexZone = catalogZone; //use catalog query access options\n            }\n            else\n            {\n                SecondaryCatalogZone secondaryCatalogZone = apexZone.SecondaryCatalogZone;\n                if (secondaryCatalogZone is not null)\n                {\n                    if (!apexZone.OverrideCatalogQueryAccess)\n                        apexZone = secondaryCatalogZone; //use secondary query access options\n                }\n            }\n\n            switch (apexZone.QueryAccess)\n            {\n                case AuthZoneQueryAccess.Allow:\n                    return true;\n\n                case AuthZoneQueryAccess.AllowOnlyPrivateNetworks:\n                    if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP))\n                        return true;\n\n                    switch (remoteIP.AddressFamily)\n                    {\n                        case AddressFamily.InterNetwork:\n                        case AddressFamily.InterNetworkV6:\n                            return NetUtilities.IsPrivateIP(remoteIP);\n\n                        default:\n                            return false;\n                    }\n\n                case AuthZoneQueryAccess.AllowOnlyZoneNameServers:\n                    if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP))\n                        return true;\n\n                    return await IsZoneNameServerAllowedAsync();\n\n                case AuthZoneQueryAccess.UseSpecifiedNetworkACL:\n                    if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP))\n                        return true;\n\n                    return NetworkAccessControl.IsAddressAllowed(remoteIP, apexZone.QueryAccessNetworkACL);\n\n                case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                    if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP))\n                        return true;\n\n                    return NetworkAccessControl.IsAddressAllowed(remoteIP, apexZone.QueryAccessNetworkACL) || await IsZoneNameServerAllowedAsync();\n\n                default:\n                    if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP))\n                        return true;\n\n                    return false;\n            }\n        }\n\n        private void ResolveCNAME(DnsQuestionRecord question, bool dnssecOk, DnsResourceRecord lastCNAME, List<DnsResourceRecord> answerRecords)\n        {\n            int queryCount = 0;\n\n            do\n            {\n                string cnameDomain = (lastCNAME.RDATA as DnsCNAMERecordData).Domain;\n                if (lastCNAME.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase))\n                    break; //loop detected\n\n                if (!_root.TryGet(cnameDomain, out AuthZoneNode zoneNode))\n                    break;\n\n                IReadOnlyList<DnsResourceRecord> records = zoneNode.QueryRecords(question.Type, dnssecOk);\n                if (records.Count < 1)\n                    break;\n\n                DnsResourceRecord lastRR = records[records.Count - 1];\n                if (lastRR.Type != DnsResourceRecordType.CNAME)\n                {\n                    answerRecords.AddRange(records);\n                    break;\n                }\n\n                foreach (DnsResourceRecord answerRecord in answerRecords)\n                {\n                    if (answerRecord.Type != DnsResourceRecordType.CNAME)\n                        continue;\n\n                    if (answerRecord.RDATA.Equals(lastRR.RDATA))\n                        return; //loop detected\n                }\n\n                answerRecords.AddRange(records);\n\n                lastCNAME = lastRR;\n            }\n            while (++queryCount < DnsServer.MAX_CNAME_HOPS);\n        }\n\n        private bool DoDNAMESubstitution(DnsQuestionRecord question, bool dnssecOk, IReadOnlyList<DnsResourceRecord> answer, out IReadOnlyList<DnsResourceRecord> newAnswer)\n        {\n            DnsResourceRecord dnameRR = answer[0];\n\n            string result = (dnameRR.RDATA as DnsDNAMERecordData).Substitute(question.Name, dnameRR.Name);\n\n            if (DnsClient.IsDomainNameValid(result))\n            {\n                DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TTL, new DnsCNAMERecordData(result));\n\n                List<DnsResourceRecord> list = new List<DnsResourceRecord>(5);\n\n                list.AddRange(answer);\n                list.Add(cnameRR);\n\n                ResolveCNAME(question, dnssecOk, cnameRR, list);\n\n                newAnswer = list;\n                return true;\n            }\n            else\n            {\n                newAnswer = answer;\n                return false;\n            }\n        }\n\n        private List<DnsResourceRecord> GetAdditionalRecords(IReadOnlyList<DnsResourceRecord> refRecords, bool dnssecOk)\n        {\n            List<DnsResourceRecord> additionalRecords = new List<DnsResourceRecord>(refRecords.Count);\n\n            foreach (DnsResourceRecord refRecord in refRecords)\n            {\n                switch (refRecord.Type)\n                {\n                    case DnsResourceRecordType.NS:\n                        IReadOnlyList<DnsResourceRecord> glueRecords = refRecord.GetAuthNSRecordInfo().GlueRecords;\n                        if (glueRecords is not null)\n                        {\n                            additionalRecords.AddRange(glueRecords);\n                        }\n                        else\n                        {\n                            ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsNSRecordData).NameServer, dnssecOk, additionalRecords);\n                        }\n                        break;\n\n                    case DnsResourceRecordType.MX:\n                        ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsMXRecordData).Exchange, dnssecOk, additionalRecords);\n                        break;\n\n                    case DnsResourceRecordType.SRV:\n                        ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsSRVRecordData).Target, dnssecOk, additionalRecords);\n                        break;\n\n                    case DnsResourceRecordType.SVCB:\n                    case DnsResourceRecordType.HTTPS:\n                        DnsSVCBRecordData svcb = refRecord.RDATA as DnsSVCBRecordData;\n                        string targetName = svcb.TargetName;\n\n                        if (svcb.SvcPriority == 0)\n                        {\n                            //For AliasMode SVCB RRs, a TargetName of \".\" indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12]\n                            if ((targetName.Length == 0) || targetName.Equals(refRecord.Name, StringComparison.OrdinalIgnoreCase))\n                                break;\n                        }\n                        else\n                        {\n                            //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]\n                            if (targetName.Length == 0)\n                                targetName = refRecord.Name;\n                        }\n\n                        ResolveAdditionalRecords(refRecord, targetName, dnssecOk, additionalRecords);\n                        break;\n                }\n            }\n\n            return additionalRecords;\n        }\n\n        private void ResolveAdditionalRecords(DnsResourceRecord refRecord, string domain, bool dnssecOk, List<DnsResourceRecord> additionalRecords)\n        {\n            int count = 0;\n\n            while (count++ < DnsServer.MAX_CNAME_HOPS)\n            {\n                AuthZone zone = _root.FindZone(domain, out _, out _, out _, out _);\n                if ((zone is null) || !zone.IsActive)\n                    break;\n\n                if (((refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) && ((refRecord.RDATA as DnsSVCBRecordData).SvcPriority == 0))\n                {\n                    //resolve SVCB/HTTPS for Alias mode refRecord\n                    IReadOnlyList<DnsResourceRecord> records = zone.QueryRecordsWildcard(refRecord.Type, dnssecOk, domain);\n                    if ((records.Count > 0) && (records[0].Type == refRecord.Type) && (records[0].RDATA is DnsSVCBRecordData svcb))\n                    {\n                        additionalRecords.AddRange(records);\n\n                        string targetName = svcb.TargetName;\n\n                        if (svcb.SvcPriority == 0)\n                        {\n                            //Alias mode\n                            if ((targetName.Length == 0) || targetName.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase))\n                                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]\n\n                            foreach (DnsResourceRecord additionalRecord in additionalRecords)\n                            {\n                                if (additionalRecord.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase))\n                                    return; //loop detected\n                            }\n\n                            //continue to resolve SVCB/HTTPS further\n                            domain = targetName;\n                            refRecord = records[0];\n                            continue;\n                        }\n                        else\n                        {\n                            //Service mode\n                            if (targetName.Length > 0)\n                            {\n                                //continue to resolve A/AAAA for target name\n                                domain = targetName;\n                                refRecord = records[0];\n                                continue;\n                            }\n\n                            //resolve A/AAAA below\n                        }\n                    }\n                }\n\n                bool hasA = false;\n                bool hasAAAA = false;\n\n                if ((refRecord.Type == DnsResourceRecordType.SRV) || (refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS))\n                {\n                    foreach (DnsResourceRecord additionalRecord in additionalRecords)\n                    {\n                        if (additionalRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                        {\n                            switch (additionalRecord.Type)\n                            {\n                                case DnsResourceRecordType.A:\n                                    hasA = true;\n                                    break;\n\n                                case DnsResourceRecordType.AAAA:\n                                    hasAAAA = true;\n                                    break;\n                            }\n                        }\n\n                        if (hasA && hasAAAA)\n                            break;\n                    }\n                }\n\n                if (!hasA)\n                {\n                    IReadOnlyList<DnsResourceRecord> records = zone.QueryRecordsWildcard(DnsResourceRecordType.A, dnssecOk, domain);\n                    if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.A))\n                        additionalRecords.AddRange(records);\n                }\n\n                if (!hasAAAA)\n                {\n                    IReadOnlyList<DnsResourceRecord> records = zone.QueryRecordsWildcard(DnsResourceRecordType.AAAA, dnssecOk, domain);\n                    if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.AAAA))\n                        additionalRecords.AddRange(records);\n                }\n\n                break;\n            }\n        }\n\n        private DnsDatagram GetReferralResponse(DnsDatagram request, bool dnssecOk, AuthZone delegationZone, ApexZone apexZone)\n        {\n            IReadOnlyList<DnsResourceRecord> authority;\n\n            if (delegationZone is StubZone)\n            {\n                authority = delegationZone.GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant query\n\n                //update last used on\n                DateTime utcNow = DateTime.UtcNow;\n\n                foreach (DnsResourceRecord record in authority)\n                    record.GetAuthGenericRecordInfo().LastUsedOn = utcNow;\n            }\n            else\n            {\n                authority = delegationZone.QueryRecords(DnsResourceRecordType.NS, false);\n\n                if (dnssecOk)\n                {\n                    IReadOnlyList<DnsResourceRecord> dsRecords = delegationZone.QueryRecords(DnsResourceRecordType.DS, true);\n                    if (dsRecords.Count > 0)\n                    {\n                        List<DnsResourceRecord> newAuthority = new List<DnsResourceRecord>(authority.Count + dsRecords.Count);\n\n                        newAuthority.AddRange(authority);\n                        newAuthority.AddRange(dsRecords);\n\n                        authority = newAuthority;\n                    }\n                    else\n                    {\n                        //add proof of non existence (NODATA) to prove DS record does not exists\n                        IReadOnlyList<DnsResourceRecord> nsecRecords;\n\n                        if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3)\n                            nsecRecords = _root.FindNSec3ProofOfNonExistenceNoData(request.Question[0].Name, delegationZone, apexZone);\n                        else\n                            nsecRecords = _root.FindNSecProofOfNonExistenceNoData(request.Question[0].Name, delegationZone);\n\n                        if (nsecRecords.Count > 0)\n                        {\n                            List<DnsResourceRecord> newAuthority = new List<DnsResourceRecord>(authority.Count + nsecRecords.Count);\n\n                            newAuthority.AddRange(authority);\n                            newAuthority.AddRange(nsecRecords);\n\n                            authority = newAuthority;\n                        }\n                    }\n                }\n            }\n\n            IReadOnlyList<DnsResourceRecord> additional = GetAdditionalRecords(authority, dnssecOk);\n\n            return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional);\n        }\n\n        private DnsDatagram GetForwarderResponse(DnsDatagram request, AuthZone zone, SubDomainZone closestZone, ApexZone forwarderZone)\n        {\n            IReadOnlyList<DnsResourceRecord> authority = null;\n\n            if (zone is not null)\n            {\n                if (zone.ContainsNameServerRecords())\n                    return GetReferralResponse(request, false, zone, forwarderZone);\n\n                authority = zone.QueryRecords(DnsResourceRecordType.FWD, false);\n            }\n\n            if (((authority is null) || (authority.Count == 0)) && (closestZone is not null))\n            {\n                if (closestZone.ContainsNameServerRecords())\n                    return GetReferralResponse(request, false, closestZone, forwarderZone);\n\n                authority = closestZone.QueryRecords(DnsResourceRecordType.FWD, false);\n            }\n\n            if ((authority is null) || (authority.Count == 0))\n            {\n                if (forwarderZone.ContainsNameServerRecords())\n                    return GetReferralResponse(request, false, forwarderZone, forwarderZone);\n\n                authority = forwarderZone.QueryRecords(DnsResourceRecordType.FWD, false);\n            }\n\n            return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority);\n        }\n\n        #endregion\n\n        #region properties\n\n        public uint DefaultRecordTtl\n        {\n            get { return _defaultRecordTtl; }\n            set { _defaultRecordTtl = value; }\n        }\n\n        public uint DefaultNsRecordTtl\n        {\n            get { return _defaultNsRecordTtl; }\n            set { _defaultNsRecordTtl = value; }\n        }\n\n        public uint DefaultSoaRecordTtl\n        {\n            get { return _defaultSoaRecordTtl; }\n            set { _defaultSoaRecordTtl = value; }\n        }\n\n        public bool UseSoaSerialDateScheme\n        {\n            get { return _useSoaSerialDateScheme; }\n            set { _useSoaSerialDateScheme = value; }\n        }\n\n        public uint MinSoaRefresh\n        {\n            get { return _minSoaRefresh; }\n            set { _minSoaRefresh = value; }\n        }\n\n        public uint MinSoaRetry\n        {\n            get { return _minSoaRetry; }\n            set { _minSoaRetry = value; }\n        }\n\n        public int TotalZones\n        { get { return _zoneIndex.Count; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ZoneManagers/BlockListZoneManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Http.Client;\n\nnamespace DnsServerCore.Dns.ZoneManagers\n{\n    public sealed class BlockListZoneManager : IDisposable\n    {\n        #region variables\n\n        readonly static char[] _popWordSeperator = new char[] { ' ', '\\t' };\n        readonly static char[] _trimSeperator = new char[] { ' ', '\\t', '*', '.' };\n\n        readonly DnsServer _dnsServer;\n        readonly string _localCacheFolder;\n\n        IReadOnlyList<string> _blockListUrls = [];\n\n        Dictionary<string, object> _allowListZone = new Dictionary<string, object>();\n        Dictionary<string, List<Uri>> _blockListZone = new Dictionary<string, List<Uri>>();\n\n        DnsSOARecordData _soaRecord;\n        DnsNSRecordData _nsRecord;\n\n        readonly IReadOnlyCollection<DnsARecordData> _aRecords = [new DnsARecordData(IPAddress.Any)];\n        readonly IReadOnlyCollection<DnsAAAARecordData> _aaaaRecords = [new DnsAAAARecordData(IPAddress.IPv6Any)];\n\n        Timer _blockListUpdateTimer;\n        DateTime _blockListLastUpdatedOn;\n        int _blockListUpdateIntervalHours = 24;\n        const int BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL = 5000;\n        const int BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL = 900000;\n\n        Timer _temporaryDisableBlockingTimer;\n        DateTime _temporaryDisableBlockingTill;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        #endregion\n\n        #region constructor\n\n        public BlockListZoneManager(DnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n\n            _localCacheFolder = Path.Combine(_dnsServer.ConfigFolder, \"blocklists\");\n\n            if (!Directory.Exists(_localCacheFolder))\n                Directory.CreateDirectory(_localCacheFolder);\n\n            UpdateServerDomain();\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveConfigFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsServer.LogManager.Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _blockListUpdateTimer?.Dispose();\n            _temporaryDisableBlockingTimer?.Dispose();\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                if (_pendingSave)\n                {\n                    try\n                    {\n                        SaveConfigFileInternal();\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                    finally\n                    {\n                        _pendingSave = false;\n                    }\n                }\n            }\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region config\n\n        public void LoadConfigFile()\n        {\n            string blockListConfigFile = Path.Combine(_dnsServer.ConfigFolder, \"blocklist.config\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(blockListConfigFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS, false);\n                }\n\n                _dnsServer.LogManager.Write(\"DNS Server block list config file was loaded: \" + blockListConfigFile);\n            }\n            catch (FileNotFoundException)\n            {\n                SaveConfigFileInternal();\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(\"DNS Server encountered an error while loading block list config file: \" + blockListConfigFile + \"\\r\\n\" + ex.ToString());\n            }\n        }\n\n        public void LoadConfig(Stream s, bool isConfigTransfer)\n        {\n            lock (_saveLock)\n            {\n                ReadConfigFrom(s, isConfigTransfer);\n\n                SaveConfigFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void SaveConfigFileInternal()\n        {\n            string blockListConfigFile = Path.Combine(_dnsServer.ConfigFolder, \"blocklist.config\");\n\n            using (MemoryStream mS = new MemoryStream())\n            {\n                //serialize config\n                WriteConfigTo(mS);\n\n                //write config\n                mS.Position = 0;\n\n                using (FileStream fS = new FileStream(blockListConfigFile, FileMode.Create, FileAccess.Write))\n                {\n                    mS.CopyTo(fS);\n                }\n            }\n\n            _dnsServer.LogManager.Write(\"DNS Server block list config file was saved: \" + blockListConfigFile);\n        }\n\n        public void SaveConfigFile()\n        {\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void ReadConfigFrom(Stream s, bool isConfigTransfer)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"BL\") //format\n                throw new InvalidDataException(\"DnsServer block list zone file format is invalid.\");\n\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    int count = bR.ReadByte();\n                    string[] blockListUrls = new string[count];\n\n                    for (int i = 0; i < count; i++)\n                        blockListUrls[i] = bR.ReadShortString();\n\n                    _blockListUpdateIntervalHours = bR.ReadInt32();\n\n                    DateTime blockListLastUpdatedOn = bR.ReadDateTime();\n                    if (!isConfigTransfer)\n                        _blockListLastUpdatedOn = blockListLastUpdatedOn;\n\n                    if (blockListUrls.Length > 0)\n                    {\n                        //load block list URLs async\n                        ThreadPool.QueueUserWorkItem(delegate (object state)\n                        {\n                            try\n                            {\n                                LoadBlockLists();\n                            }\n                            catch (Exception ex)\n                            {\n                                _dnsServer.LogManager.Write(ex);\n                            }\n                        });\n                    }\n\n                    ApplyBlockListUrls(blockListUrls);\n                    ApplyBlockListUpdateInterval();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"DnsServer block list zone file version not supported.\");\n            }\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"BL\")); //format\n            bW.Write((byte)1); //version\n\n            bW.Write(Convert.ToByte(_blockListUrls.Count));\n\n            foreach (string blockListUrl in _blockListUrls)\n                bW.WriteShortString(blockListUrl);\n\n            bW.Write(_blockListUpdateIntervalHours);\n            bW.Write(_blockListLastUpdatedOn);\n        }\n\n        #endregion\n\n        #region private\n\n        internal void UpdateServerDomain()\n        {\n            _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, _dnsServer.BlockingAnswerTtl);\n            _nsRecord = new DnsNSRecordData(_dnsServer.ServerDomain);\n        }\n\n        private string GetBlockListFilePath(Uri blockListUrl)\n        {\n            return Path.Combine(_localCacheFolder, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(blockListUrl.AbsoluteUri))).ToLowerInvariant());\n        }\n\n        private static string PopWord(ref string line)\n        {\n            if (line.Length == 0)\n                return line;\n\n            line = line.TrimStart(_popWordSeperator);\n\n            int i = line.IndexOfAny(_popWordSeperator);\n            string word;\n\n            if (i < 0)\n            {\n                word = line;\n                line = \"\";\n            }\n            else\n            {\n                word = line.Substring(0, i);\n                line = line.Substring(i + 1);\n            }\n\n            return word;\n        }\n\n        private Queue<string> ReadListFile(Uri listUrl, bool isAllowList, out Queue<string> exceptionDomains)\n        {\n            Queue<string> domains = new Queue<string>();\n            exceptionDomains = new Queue<string>();\n\n            try\n            {\n                _dnsServer.LogManager.Write(\"DNS Server is reading \" + (isAllowList ? \"allow\" : \"block\") + \" list from: \" + listUrl.AbsoluteUri);\n\n                string listFilePath = GetBlockListFilePath(listUrl);\n\n                if (listUrl.IsFile)\n                {\n                    if (!File.Exists(listFilePath) || (File.GetLastWriteTimeUtc(listUrl.LocalPath) > File.GetLastWriteTimeUtc(listFilePath)))\n                    {\n                        File.Copy(listUrl.LocalPath, listFilePath, true);\n\n                        _dnsServer.LogManager.Write(\"DNS Server successfully downloaded \" + (isAllowList ? \"allow\" : \"block\") + \" list (\" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + \"): \" + listUrl.AbsoluteUri);\n                    }\n                }\n\n                using (FileStream fS = new FileStream(listFilePath, FileMode.Open, FileAccess.Read))\n                {\n                    //parse hosts file and populate block zone\n                    StreamReader sR = new StreamReader(fS, true);\n\n                    string line;\n                    string firstWord;\n                    string secondWord;\n                    string hostname;\n                    string domain;\n                    string options;\n                    int i;\n\n                    while (true)\n                    {\n                        line = sR.ReadLine();\n                        if (line is null)\n                            break; //eof\n\n                        line = line.TrimStart(_trimSeperator);\n\n                        if (line.Length == 0)\n                            continue; //skip empty line\n\n                        if (line.StartsWith('#') || line.StartsWith('!'))\n                            continue; //skip comment line\n\n                        if (line.StartsWith(\"||\"))\n                        {\n                            //adblock format\n                            i = line.IndexOf('^');\n                            if (i > -1)\n                            {\n                                domain = line.Substring(2, i - 2);\n                                options = line.Substring(i + 1);\n\n                                if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains(\"doc\") || options.Contains(\"all\")))) && DnsClient.IsDomainNameValid(domain))\n                                    domains.Enqueue(domain.ToLowerInvariant());\n                            }\n                            else\n                            {\n                                domain = line.Substring(2);\n\n                                if (DnsClient.IsDomainNameValid(domain))\n                                    domains.Enqueue(domain.ToLowerInvariant());\n                            }\n                        }\n                        else if (line.StartsWith(\"@@||\"))\n                        {\n                            //adblock format - exception syntax\n                            i = line.IndexOf('^');\n                            if (i > -1)\n                            {\n                                domain = line.Substring(4, i - 4);\n                                options = line.Substring(i + 1);\n\n                                if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains(\"doc\") || options.Contains(\"all\")))) && DnsClient.IsDomainNameValid(domain))\n                                    exceptionDomains.Enqueue(domain.ToLowerInvariant());\n                            }\n                            else\n                            {\n                                domain = line.Substring(4);\n\n                                if (DnsClient.IsDomainNameValid(domain))\n                                    exceptionDomains.Enqueue(domain.ToLowerInvariant());\n                            }\n                        }\n                        else\n                        {\n                            //hosts file format\n                            firstWord = PopWord(ref line);\n\n                            if (line.Length == 0)\n                            {\n                                hostname = firstWord;\n                            }\n                            else\n                            {\n                                secondWord = PopWord(ref line);\n\n                                if ((secondWord.Length == 0) || secondWord.StartsWith('#'))\n                                    hostname = firstWord;\n                                else\n                                    hostname = secondWord;\n                            }\n\n                            hostname = hostname.Trim('.').ToLowerInvariant();\n\n                            switch (hostname)\n                            {\n                                case \"\":\n                                case \"localhost\":\n                                case \"localhost.localdomain\":\n                                case \"local\":\n                                case \"broadcasthost\":\n                                case \"ip6-localhost\":\n                                case \"ip6-loopback\":\n                                case \"ip6-localnet\":\n                                case \"ip6-mcastprefix\":\n                                case \"ip6-allnodes\":\n                                case \"ip6-allrouters\":\n                                case \"ip6-allhosts\":\n                                    continue; //skip these hostnames\n                            }\n\n                            if (!DnsClient.IsDomainNameValid(hostname))\n                                continue;\n\n                            if (IPAddress.TryParse(hostname, out _))\n                                continue; //skip line when hostname is IP address\n\n                            domains.Enqueue(hostname);\n                        }\n                    }\n                }\n\n                _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);\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(\"DNS Server failed to read \" + (isAllowList ? \"allow\" : \"block\") + \" list from: \" + listUrl.AbsoluteUri + \"\\r\\n\" + ex.ToString());\n            }\n\n            return domains;\n        }\n\n        private List<Uri> IsZoneBlocked(string domain, out string blockedDomain)\n        {\n            domain = domain.ToLowerInvariant();\n\n            do\n            {\n                if (_blockListZone.TryGetValue(domain, out List<Uri> blockLists))\n                {\n                    //found zone blocked\n                    blockedDomain = domain;\n                    return blockLists;\n                }\n\n                domain = AuthZoneManager.GetParentZone(domain);\n            }\n            while (domain is not null);\n\n            blockedDomain = null;\n            return null;\n        }\n\n        private bool IsZoneAllowed(string domain)\n        {\n            domain = domain.ToLowerInvariant();\n\n            do\n            {\n                if (_allowListZone.TryGetValue(domain, out _))\n                    return true;\n\n                domain = AuthZoneManager.GetParentZone(domain);\n            }\n            while (domain is not null);\n\n            return false;\n        }\n\n        private void ApplyBlockListUrls(IReadOnlyList<string> blockListUrls)\n        {\n            bool blockListUrlsUpdated = !blockListUrls.HasSameItems(_blockListUrls);\n\n            _blockListUrls = blockListUrls;\n\n            if ((_blockListUpdateIntervalHours > 0) && (_blockListUrls.Count > 0))\n            {\n                if (_blockListUpdateTimer is null)\n                    StartBlockListUpdateTimer(blockListUrlsUpdated);\n                else if (blockListUrlsUpdated)\n                    ForceUpdateBlockLists(true);\n            }\n            else\n            {\n                StopBlockListUpdateTimer();\n            }\n\n            if (_blockListUrls.Count < 1)\n                Flush();\n        }\n\n        private void ApplyBlockListUpdateInterval()\n        {\n            if ((_blockListUpdateIntervalHours > 0) && (_blockListUrls.Count > 0))\n            {\n                if (_blockListUpdateTimer is null)\n                    StartBlockListUpdateTimer(false);\n            }\n            else\n            {\n                StopBlockListUpdateTimer();\n            }\n        }\n\n        private void Flush()\n        {\n            _allowListZone = new Dictionary<string, object>();\n            _blockListZone = new Dictionary<string, List<Uri>>();\n        }\n\n        private async Task<bool> UpdateBlockListsAsync(bool forceReload)\n        {\n            bool downloaded = false;\n            bool notModified = false;\n\n            async Task DownloadListUrlAsync(Uri listUrl, bool isAllowList)\n            {\n                try\n                {\n                    _dnsServer.LogManager.Write(\"DNS Server is downloading \" + (isAllowList ? \"allow\" : \"block\") + \" list: \" + listUrl.AbsoluteUri);\n\n                    string listFilePath = GetBlockListFilePath(listUrl);\n\n                    if (listUrl.IsFile)\n                    {\n                        if (File.Exists(listFilePath))\n                        {\n                            if (File.GetLastWriteTimeUtc(listUrl.LocalPath) <= File.GetLastWriteTimeUtc(listFilePath))\n                            {\n                                notModified = true;\n                                _dnsServer.LogManager.Write(\"DNS Server successfully checked for a new update of the \" + (isAllowList ? \"allow\" : \"block\") + \" list: \" + listUrl.AbsoluteUri);\n                                return;\n                            }\n                        }\n\n                        File.Copy(listUrl.LocalPath, listFilePath, true);\n\n                        downloaded = true;\n                        _dnsServer.LogManager.Write(\"DNS Server successfully downloaded \" + (isAllowList ? \"allow\" : \"block\") + \" list (\" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + \"): \" + listUrl.AbsoluteUri);\n                    }\n                    else\n                    {\n                        HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n                        handler.Proxy = _dnsServer.Proxy;\n                        handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                        handler.DnsClient = _dnsServer;\n\n                        using (HttpClient http = new HttpClient(handler))\n                        {\n                            if (File.Exists(listFilePath))\n                                http.DefaultRequestHeaders.IfModifiedSince = File.GetLastWriteTimeUtc(listFilePath);\n\n                            HttpResponseMessage httpResponse = await http.GetAsync(listUrl);\n                            switch (httpResponse.StatusCode)\n                            {\n                                case HttpStatusCode.OK:\n                                    {\n                                        string listDownloadFilePath = listFilePath + \".downloading\";\n\n                                        using (FileStream fS = new FileStream(listDownloadFilePath, FileMode.Create, FileAccess.Write))\n                                        {\n                                            using (Stream httpStream = await httpResponse.Content.ReadAsStreamAsync())\n                                            {\n                                                await httpStream.CopyToAsync(fS);\n                                            }\n                                        }\n\n                                        File.Move(listDownloadFilePath, listFilePath, true);\n\n                                        if (httpResponse.Content.Headers.LastModified != null)\n                                            File.SetLastWriteTimeUtc(listFilePath, httpResponse.Content.Headers.LastModified.Value.UtcDateTime);\n\n                                        downloaded = true;\n                                        _dnsServer.LogManager.Write(\"DNS Server successfully downloaded \" + (isAllowList ? \"allow\" : \"block\") + \" list (\" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + \"): \" + listUrl.AbsoluteUri);\n                                    }\n                                    break;\n\n                                case HttpStatusCode.NotModified:\n                                    {\n                                        notModified = true;\n                                        _dnsServer.LogManager.Write(\"DNS Server successfully checked for a new update of the \" + (isAllowList ? \"allow\" : \"block\") + \" list: \" + listUrl.AbsoluteUri);\n                                    }\n                                    break;\n\n                                default:\n                                    throw new HttpRequestException((int)httpResponse.StatusCode + \" \" + httpResponse.ReasonPhrase);\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _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());\n                }\n            }\n\n            List<Task> tasks = new List<Task>();\n\n            foreach (string blockListUrl in _blockListUrls)\n            {\n                if (blockListUrl.TrimStart().StartsWith('#'))\n                    continue; //skip comment line\n\n                if (blockListUrl.StartsWith('!'))\n                    tasks.Add(DownloadListUrlAsync(new Uri(blockListUrl.Substring(1)), true));\n                else\n                    tasks.Add(DownloadListUrlAsync(new Uri(blockListUrl), false));\n            }\n\n            await Task.WhenAll(tasks);\n\n            if (downloaded || forceReload)\n            {\n                LoadBlockLists();\n\n                //force GC collection to remove old zone data from memory quickly\n                GC.Collect();\n            }\n\n            return downloaded || notModified;\n        }\n\n        private void ForceUpdateBlockLists(bool forceReload)\n        {\n            ThreadPool.QueueUserWorkItem(async delegate (object state)\n            {\n                try\n                {\n                    if (await UpdateBlockListsAsync(forceReload))\n                    {\n                        //block lists were updated\n                        //save last updated on time\n                        _blockListLastUpdatedOn = DateTime.UtcNow;\n                        SaveConfigFile();\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            });\n        }\n\n        private void StartBlockListUpdateTimer(bool forceUpdateAndReload)\n        {\n            if (_blockListUpdateTimer is null)\n            {\n                if (forceUpdateAndReload)\n                    _blockListLastUpdatedOn = default;\n\n                _blockListUpdateTimer = new Timer(async delegate (object state)\n                {\n                    try\n                    {\n                        if (DateTime.UtcNow > _blockListLastUpdatedOn.AddHours(_blockListUpdateIntervalHours))\n                        {\n                            if (await UpdateBlockListsAsync(_blockListLastUpdatedOn == default))\n                            {\n                                //block lists were updated\n                                //save last updated on time\n                                _blockListLastUpdatedOn = DateTime.UtcNow;\n                                SaveConfigFile();\n                            }\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server encountered an error while updating block lists.\\r\\n\" + ex.ToString());\n                    }\n                    finally\n                    {\n                        try\n                        {\n                            _blockListUpdateTimer?.Change(BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);\n                        }\n                        catch (ObjectDisposedException)\n                        { }\n                    }\n                }, null, BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void StopBlockListUpdateTimer()\n        {\n            if (_blockListUpdateTimer is not null)\n            {\n                _blockListUpdateTimer.Dispose();\n                _blockListUpdateTimer = null;\n            }\n        }\n\n        private void LoadBlockLists()\n        {\n            _dnsServer.LogManager.Write(\"DNS Server is loading block list zone...\");\n\n            List<Uri> allowListUrls = new List<Uri>();\n            List<Uri> blockListUrls = new List<Uri>();\n\n            foreach (string listUri in _blockListUrls)\n            {\n                if (listUri.TrimStart().StartsWith('#'))\n                    continue; //skip comment line\n\n                if (listUri.StartsWith('!'))\n                    allowListUrls.Add(new Uri(listUri.Substring(1)));\n                else\n                    blockListUrls.Add(new Uri(listUri));\n            }\n\n            Dictionary<Uri, Queue<string>> allowListQueues = new Dictionary<Uri, Queue<string>>(allowListUrls.Count);\n            Dictionary<Uri, Queue<string>> blockListQueues = new Dictionary<Uri, Queue<string>>(blockListUrls.Count);\n            int totalAllowedDomains = 0;\n            int totalBlockedDomains = 0;\n\n            //read all allow lists in a queue\n            foreach (Uri allowListUrl in allowListUrls)\n            {\n                if (!allowListQueues.ContainsKey(allowListUrl))\n                {\n                    Queue<string> allowListQueue = ReadListFile(allowListUrl, true, out Queue<string> blockListQueue);\n\n                    totalAllowedDomains += allowListQueue.Count;\n                    allowListQueues.Add(allowListUrl, allowListQueue);\n\n                    totalBlockedDomains += blockListQueue.Count;\n                    blockListQueues.Add(allowListUrl, blockListQueue);\n                }\n            }\n\n            //read all block lists in a queue\n            foreach (Uri blockListUrl in blockListUrls)\n            {\n                if (!blockListQueues.ContainsKey(blockListUrl))\n                {\n                    Queue<string> blockListQueue = ReadListFile(blockListUrl, false, out Queue<string> allowListQueue);\n\n                    totalBlockedDomains += blockListQueue.Count;\n                    blockListQueues.Add(blockListUrl, blockListQueue);\n\n                    totalAllowedDomains += allowListQueue.Count;\n                    allowListQueues.Add(blockListUrl, allowListQueue);\n                }\n            }\n\n            //load block list zone\n            Dictionary<string, object> allowListZone = new Dictionary<string, object>(totalAllowedDomains);\n\n            foreach (KeyValuePair<Uri, Queue<string>> allowListQueue in allowListQueues)\n            {\n                Queue<string> queue = allowListQueue.Value;\n\n                while (queue.Count > 0)\n                {\n                    string domain = queue.Dequeue();\n\n                    allowListZone.TryAdd(domain, null);\n                }\n            }\n\n            Dictionary<string, List<Uri>> blockListZone = new Dictionary<string, List<Uri>>(totalBlockedDomains);\n\n            foreach (KeyValuePair<Uri, Queue<string>> blockListQueue in blockListQueues)\n            {\n                Queue<string> queue = blockListQueue.Value;\n\n                while (queue.Count > 0)\n                {\n                    string domain = queue.Dequeue();\n\n                    if (!blockListZone.TryGetValue(domain, out List<Uri> blockLists))\n                    {\n                        blockLists = new List<Uri>(2);\n                        blockListZone.Add(domain, blockLists);\n                    }\n\n                    blockLists.Add(blockListQueue.Key);\n                }\n            }\n\n            //set new allowed and blocked zones\n            _allowListZone = allowListZone;\n            _blockListZone = blockListZone;\n\n            _dnsServer.LogManager.Write(\"DNS Server block list zone was loaded successfully.\");\n        }\n\n        #endregion\n\n        #region public\n\n        public bool IsAllowed(DnsDatagram request)\n        {\n            if (_allowListZone.Count < 1)\n                return false;\n\n            return IsZoneAllowed(request.Question[0].Name);\n        }\n\n        public DnsDatagram Query(DnsDatagram request)\n        {\n            if (_blockListZone.Count < 1)\n                return null;\n\n            DnsQuestionRecord question = request.Question[0];\n\n            List<Uri> blockLists = IsZoneBlocked(question.Name, out string blockedDomain);\n            if (blockLists is null)\n                return null; //zone not blocked\n\n            //zone is blocked\n            if (_dnsServer.AllowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT))\n            {\n                //return meta data\n                DnsResourceRecord[] answer = new DnsResourceRecord[blockLists.Count];\n\n                for (int i = 0; i < answer.Length; i++)\n                    answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _dnsServer.BlockingAnswerTtl, new DnsTXTRecordData(\"source=block-list-zone; blockListUrl=\" + blockLists[i].AbsoluteUri + \"; domain=\" + blockedDomain));\n\n                return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer);\n            }\n            else\n            {\n                EDnsOption[] options = null;\n\n                if (_dnsServer.AllowTxtBlockingReport && (request.EDNS is not null))\n                {\n                    options = new EDnsOption[blockLists.Count];\n\n                    for (int i = 0; i < options.Length; i++)\n                        options[i] = new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, \"source=block-list-zone; blockListUrl=\" + blockLists[i].AbsoluteUri + \"; domain=\" + blockedDomain));\n                }\n\n                IReadOnlyCollection<DnsARecordData> aRecords;\n                IReadOnlyCollection<DnsAAAARecordData> aaaaRecords;\n\n                switch (_dnsServer.BlockingType)\n                {\n                    case DnsServerBlockingType.AnyAddress:\n                        aRecords = _aRecords;\n                        aaaaRecords = _aaaaRecords;\n                        break;\n\n                    case DnsServerBlockingType.CustomAddress:\n                        aRecords = _dnsServer.CustomBlockingARecords;\n                        aaaaRecords = _dnsServer.CustomBlockingAAAARecords;\n                        break;\n\n                    case DnsServerBlockingType.NxDomain:\n                        string parentDomain = AuthZoneManager.GetParentZone(blockedDomain);\n                        if (parentDomain is null)\n                            parentDomain = string.Empty;\n\n                        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);\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n\n                IReadOnlyList<DnsResourceRecord> answer = null;\n                IReadOnlyList<DnsResourceRecord> authority = null;\n\n                switch (question.Type)\n                {\n                    case DnsResourceRecordType.A:\n                        {\n                            if (aRecords.Count > 0)\n                            {\n                                DnsResourceRecord[] rrList = new DnsResourceRecord[aRecords.Count];\n                                int i = 0;\n\n                                foreach (DnsARecordData record in aRecords)\n                                    rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _dnsServer.BlockingAnswerTtl, record);\n\n                                answer = rrList;\n                            }\n                            else\n                            {\n                                authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)];\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.AAAA:\n                        {\n                            if (aaaaRecords.Count > 0)\n                            {\n                                DnsResourceRecord[] rrList = new DnsResourceRecord[aaaaRecords.Count];\n                                int i = 0;\n\n                                foreach (DnsAAAARecordData record in aaaaRecords)\n                                    rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _dnsServer.BlockingAnswerTtl, record);\n\n                                answer = rrList;\n                            }\n                            else\n                            {\n                                authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)];\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NS:\n                        if (question.Name.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase))\n                            answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.NS, question.Class, _dnsServer.BlockingAnswerTtl, _nsRecord)];\n                        else\n                            authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)];\n\n                        break;\n\n                    case DnsResourceRecordType.SOA:\n                        answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)];\n                        break;\n\n                    default:\n                        authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)];\n                        break;\n                }\n\n                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);\n            }\n        }\n\n        public void ForceUpdateBlockLists()\n        {\n            ForceUpdateBlockLists(false);\n        }\n\n        public void TemporaryDisableBlocking(int minutes, IPEndPoint userEP, string username)\n        {\n            Timer temporaryDisableBlockingTimer = _temporaryDisableBlockingTimer;\n            if (temporaryDisableBlockingTimer is not null)\n                temporaryDisableBlockingTimer.Dispose();\n\n            Timer newTemporaryDisableBlockingTimer = new Timer(delegate (object state)\n            {\n                try\n                {\n                    _dnsServer.EnableBlocking = true;\n                    _dnsServer.LogManager.Write(userEP, \"[\" + username + \"] Blocking was enabled after \" + minutes + \" minute(s) being temporarily disabled.\");\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n                }\n            });\n\n            Timer originalTimer = Interlocked.CompareExchange(ref _temporaryDisableBlockingTimer, newTemporaryDisableBlockingTimer, temporaryDisableBlockingTimer);\n            if (ReferenceEquals(originalTimer, temporaryDisableBlockingTimer))\n            {\n                newTemporaryDisableBlockingTimer.Change(minutes * 60 * 1000, Timeout.Infinite);\n                _dnsServer.EnableBlocking = false;\n                _temporaryDisableBlockingTill = DateTime.UtcNow.AddMinutes(minutes);\n\n                _dnsServer.LogManager.Write(userEP, \"[\" + username + \"] Blocking was temporarily disabled for \" + minutes + \" minute(s).\");\n            }\n            else\n            {\n                newTemporaryDisableBlockingTimer.Dispose();\n            }\n        }\n\n        public void StopTemporaryDisableBlockingTimer()\n        {\n            Timer temporaryDisableBlockingTimer = _temporaryDisableBlockingTimer;\n            if (temporaryDisableBlockingTimer is not null)\n                temporaryDisableBlockingTimer.Dispose();\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyList<string> BlockListUrls\n        {\n            get { return _blockListUrls; }\n            set\n            {\n                if (value is null)\n                {\n                    value = [];\n                }\n                else if (value.Count > 255)\n                {\n                    throw new ArgumentException(\"Cannot configure more than 255 block list URLs.\", nameof(BlockListUrls));\n                }\n                else\n                {\n                    List<string> uniqueList = new List<string>(value.Count);\n                    int commentCount = 0;\n\n                    foreach (string url in value)\n                    {\n                        if (url.Length > 255)\n                            throw new ArgumentException(\"Block list URL (or comment line) length cannot exceed 255 characters.\", nameof(BlockListUrls));\n\n                        if (url.TrimStart().StartsWith('#'))\n                        {\n                            uniqueList.Add(url);\n                            commentCount++;\n                            continue;\n                        }\n\n                        if (!uniqueList.Contains(url))\n                            uniqueList.Add(url);\n                    }\n\n                    if (uniqueList.Count == commentCount)\n                        uniqueList = [];\n\n                    value = uniqueList;\n                }\n\n                ApplyBlockListUrls(value);\n            }\n        }\n\n        public int BlockListUpdateIntervalHours\n        {\n            get { return _blockListUpdateIntervalHours; }\n            set\n            {\n                if ((value < 0) || (value > 168))\n                    throw new ArgumentOutOfRangeException(nameof(BlockListUpdateIntervalHours), \"Value must be between 1 hour and 168 hours (7 days) or 0 to disable automatic update.\");\n\n                _blockListUpdateIntervalHours = value;\n\n                ApplyBlockListUpdateInterval();\n            }\n        }\n\n        public bool BlockListUpdateEnabled\n        { get { return _blockListUpdateTimer is not null; } }\n\n        public DateTime BlockListLastUpdatedOn\n        {\n            get { return _blockListLastUpdatedOn; }\n            internal set\n            {\n                _blockListLastUpdatedOn = value;\n            }\n        }\n\n        public DateTime TemporaryDisableBlockingTill\n        { get { return _temporaryDisableBlockingTill; } }\n\n        public int TotalZonesAllowed\n        { get { return _allowListZone.Count; } }\n\n        public int TotalZonesBlocked\n        { get { return _blockListZone.Count; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ZoneManagers/BlockedZoneManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Threading;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ZoneManagers\n{\n    public sealed class BlockedZoneManager\n    {\n        #region variables\n\n        readonly DnsServer _dnsServer;\n\n        AuthZoneManager _zoneManager;\n\n        readonly DnsSOARecordDataExtended _soaRecord;\n        readonly DnsNSRecordDataExtended _nsRecord;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        #endregion\n\n        #region constructor\n\n        public BlockedZoneManager(DnsServer dnsServer)\n        {\n            _dnsServer = dnsServer;\n\n            _zoneManager = new AuthZoneManager(_dnsServer);\n\n            _soaRecord = new DnsSOARecordDataExtended(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, _dnsServer.BlockingAnswerTtl);\n            _nsRecord = new DnsNSRecordDataExtended(_dnsServer.ServerDomain);\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveZoneFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsServer.LogManager.Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                if (_pendingSave)\n                {\n                    try\n                    {\n                        SaveZoneFileInternal();\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                    finally\n                    {\n                        _pendingSave = false;\n                    }\n                }\n            }\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region zone file\n\n        public void LoadBlockedZoneFile()\n        {\n            string blockedZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"blocked.config\");\n\n            try\n            {\n                string oldCustomBlockedZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"custom-blocked.config\");\n                if (File.Exists(oldCustomBlockedZoneFile))\n                {\n                    if (File.Exists(blockedZoneFile))\n                        File.Delete(blockedZoneFile);\n\n                    File.Move(oldCustomBlockedZoneFile, blockedZoneFile);\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n\n            try\n            {\n                using (FileStream fS = new FileStream(blockedZoneFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS);\n                }\n\n                _dnsServer.LogManager.Write(\"DNS Server blocked zone file was loaded: \" + blockedZoneFile);\n            }\n            catch (FileNotFoundException)\n            {\n                SaveZoneFileInternal();\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(\"DNS Server encountered an error while loading blocked zone file: \" + blockedZoneFile + \"\\r\\n\" + ex.ToString());\n            }\n        }\n\n        public void LoadBlockedZone(Stream s)\n        {\n            lock (_saveLock)\n            {\n                ReadConfigFrom(s);\n\n                SaveZoneFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void SaveZoneFileInternal()\n        {\n            string blockedZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"blocked.config\");\n\n            using (FileStream fS = new FileStream(blockedZoneFile, FileMode.Create, FileAccess.Write))\n            {\n                WriteConfigTo(fS);\n            }\n\n            _dnsServer.LogManager.Write(\"DNS Server blocked zone file was saved: \" + blockedZoneFile);\n        }\n\n        public void SaveZoneFile()\n        {\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void ReadConfigFrom(Stream s)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"BZ\") //format\n                throw new InvalidDataException(\"DnsServer blocked zone file format is invalid.\");\n\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    int length = bR.ReadInt32();\n                    int i = 0;\n\n                    AuthZoneManager zoneManager = new AuthZoneManager(_dnsServer);\n\n                    zoneManager.LoadSpecialPrimaryZones(delegate ()\n                    {\n                        if (i++ < length)\n                            return bR.ReadShortString();\n\n                        return null;\n                    }, _soaRecord, _nsRecord);\n\n                    _zoneManager = zoneManager;\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"DnsServer blocked zone file version not supported.\");\n            }\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            IReadOnlyList<AuthZoneInfo> blockedZones = _zoneManager.GetAllZones();\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"BZ\")); //format\n            bW.Write((byte)1); //version\n\n            bW.Write(blockedZones.Count);\n\n            foreach (AuthZoneInfo zone in blockedZones)\n                bW.WriteShortString(zone.Name);\n        }\n\n        #endregion\n\n        #region private\n\n        internal void UpdateServerDomain()\n        {\n            _soaRecord.UpdatePrimaryNameServerAndMinimum(_dnsServer.ServerDomain, _dnsServer.BlockingAnswerTtl);\n            _nsRecord.UpdateNameServer(_dnsServer.ServerDomain);\n        }\n\n        #endregion\n\n        #region public\n\n        public void ImportZones(string[] domains)\n        {\n            _zoneManager.LoadSpecialPrimaryZones(domains, _soaRecord, _nsRecord);\n        }\n\n        public bool BlockZone(string domain)\n        {\n            if (_zoneManager.CreateSpecialPrimaryZone(domain, _soaRecord, _nsRecord) != null)\n                return true;\n\n            return false;\n        }\n\n        public bool DeleteZone(string domain)\n        {\n            if (_zoneManager.DeleteZone(domain))\n                return true;\n\n            return false;\n        }\n\n        public void Flush()\n        {\n            _zoneManager.Flush();\n        }\n\n        public IReadOnlyList<AuthZoneInfo> GetAllZones()\n        {\n            return _zoneManager.GetAllZones();\n        }\n\n        public void ListAllRecords(string domain, List<DnsResourceRecord> records)\n        {\n            _zoneManager.ListAllRecords(domain, domain, records);\n        }\n\n        public void ListSubDomains(string domain, List<string> subDomains)\n        {\n            _zoneManager.ListSubDomains(domain, subDomains);\n        }\n\n        public DnsDatagram Query(DnsDatagram request)\n        {\n            if (_zoneManager.TotalZones < 1)\n                return null;\n\n            return _zoneManager.Query(request, false);\n        }\n\n        #endregion\n\n        #region properties\n\n        internal DnsSOARecordData DnsSOARecord\n        { get { return _soaRecord; } }\n\n        public int TotalZonesBlocked\n        { get { return _zoneManager.TotalZones; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/ZoneManagers/CacheZoneManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Trees;\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.ZoneManagers\n{\n    public sealed class CacheZoneManager : DnsCache, IDisposable\n    {\n        #region variables\n\n        public const uint FAILURE_RECORD_TTL = 10u;\n        public const uint NEGATIVE_RECORD_TTL = 300u;\n        public const uint MINIMUM_RECORD_TTL = 10u;\n        public const uint MAXIMUM_RECORD_TTL = 7 * 24 * 60 * 60;\n        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\n        public const uint SERVE_STALE_ANSWER_TTL = 30; //as per https://www.rfc-editor.org/rfc/rfc8767.html suggestion\n        public const uint SERVE_STALE_RESET_TTL = 30; //as per https://www.rfc-editor.org/rfc/rfc8767.html suggestion\n\n        const uint SERVE_STALE_MIN_RESET_TTL = 10;\n        const uint SERVE_STALE_MAX_RESET_TTL = 900;\n\n        readonly DnsServer _dnsServer;\n\n        readonly CacheZoneTree _root = new CacheZoneTree();\n\n        uint _serveStaleResetTtl = SERVE_STALE_RESET_TTL;\n        long _maximumEntries;\n        long _totalEntries;\n\n        Timer _cacheMaintenanceTimer;\n        readonly object _cacheMaintenanceTimerLock = new object();\n        const int CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL = 5 * 60 * 1000;\n        const int CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL = 5 * 60 * 1000;\n\n        #endregion\n\n        #region constructor\n\n        public CacheZoneManager(DnsServer dnsServer)\n            : base(FAILURE_RECORD_TTL, NEGATIVE_RECORD_TTL, MINIMUM_RECORD_TTL, MAXIMUM_RECORD_TTL, SERVE_STALE_TTL, SERVE_STALE_ANSWER_TTL)\n        {\n            _dnsServer = dnsServer;\n\n            _cacheMaintenanceTimer = new Timer(CacheMaintenanceTimerCallback, null, CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL, Timeout.Infinite);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            lock (_cacheMaintenanceTimerLock)\n            {\n                if (_cacheMaintenanceTimer is not null)\n                {\n                    _cacheMaintenanceTimer.Dispose();\n                    _cacheMaintenanceTimer = null;\n                }\n            }\n\n            _disposed = true;\n        }\n\n        #endregion\n\n        #region zone file\n\n        public void LoadCacheZoneFile()\n        {\n            string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"cache.bin\");\n\n            if (!File.Exists(cacheZoneFile))\n                return;\n\n            _dnsServer.LogManager.Write(\"Loading DNS Cache from disk...\");\n\n            using (FileStream fS = new FileStream(cacheZoneFile, FileMode.Open, FileAccess.Read))\n            {\n                BinaryReader bR = new BinaryReader(fS);\n\n                if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"CZ\")\n                    throw new InvalidDataException(\"CacheZoneManager format is invalid.\");\n\n                int version = bR.ReadByte();\n                switch (version)\n                {\n                    case 1:\n                        int addedEntries = 0;\n\n                        try\n                        {\n                            bool serveStale = _dnsServer.ServeStale;\n\n                            while (bR.BaseStream.Position < bR.BaseStream.Length)\n                            {\n                                CacheZone zone = CacheZone.ReadFrom(bR, serveStale);\n                                if (!zone.IsEmpty)\n                                {\n                                    if (_root.TryAdd(zone.Name, zone))\n                                        addedEntries += zone.TotalEntries;\n                                }\n                            }\n                        }\n                        finally\n                        {\n                            if (addedEntries > 0)\n                                Interlocked.Add(ref _totalEntries, addedEntries);\n                        }\n                        break;\n\n                    default:\n                        throw new InvalidDataException(\"CacheZoneManager format version not supported: \" + version);\n                }\n            }\n\n            _dnsServer.LogManager.Write(\"DNS Cache was loaded from disk successfully.\");\n        }\n\n        public void SaveCacheZoneFile()\n        {\n            _dnsServer.LogManager.Write(\"Saving DNS Cache to disk...\");\n\n            string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"cache.bin\");\n\n            using (FileStream fS = new FileStream(cacheZoneFile, FileMode.Create, FileAccess.Write))\n            {\n                BinaryWriter bW = new BinaryWriter(fS);\n\n                bW.Write(Encoding.ASCII.GetBytes(\"CZ\")); //format\n                bW.Write((byte)1); //version\n\n                foreach (CacheZone zone in _root)\n                    zone.WriteTo(bW);\n            }\n\n            _dnsServer.LogManager.Write(\"DNS Cache was saved to disk successfully.\");\n        }\n\n        public void DeleteCacheZoneFile()\n        {\n            string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, \"cache.bin\");\n\n            if (File.Exists(cacheZoneFile))\n                File.Delete(cacheZoneFile);\n        }\n\n        #endregion\n\n        #region protected\n\n        protected override void CacheRecords(IReadOnlyList<DnsResourceRecord> resourceRecords, NetworkAddress eDnsClientSubnet, DnsDatagramMetadata responseMetadata)\n        {\n            List<DnsResourceRecord> dnameRecords = null;\n\n            //read and set glue records from base class; also collect any DNAME records found\n            foreach (DnsResourceRecord resourceRecord in resourceRecords)\n            {\n                DnsResourceRecordInfo recordInfo = GetRecordInfo(resourceRecord);\n\n                IReadOnlyList<DnsResourceRecord> glueRecords = recordInfo.GlueRecords;\n                IReadOnlyList<DnsResourceRecord> rrsigRecords = recordInfo.RRSIGRecords;\n                IReadOnlyList<DnsResourceRecord> nsecRecords = recordInfo.NSECRecords;\n\n                CacheRecordInfo rrInfo = resourceRecord.GetCacheRecordInfo();\n\n                rrInfo.GlueRecords = glueRecords;\n                rrInfo.RRSIGRecords = rrsigRecords;\n                rrInfo.NSECRecords = nsecRecords;\n                rrInfo.EDnsClientSubnet = eDnsClientSubnet;\n                rrInfo.ResponseMetadata = responseMetadata;\n\n                if (glueRecords is not null)\n                {\n                    foreach (DnsResourceRecord glueRecord in glueRecords)\n                    {\n                        IReadOnlyList<DnsResourceRecord> glueRRSIGRecords = GetRecordInfo(glueRecord).RRSIGRecords;\n                        if (glueRRSIGRecords is not null)\n                            glueRecord.GetCacheRecordInfo().RRSIGRecords = glueRRSIGRecords;\n                    }\n                }\n\n                if (nsecRecords is not null)\n                {\n                    foreach (DnsResourceRecord nsecRecord in nsecRecords)\n                    {\n                        IReadOnlyList<DnsResourceRecord> nsecRRSIGRecords = GetRecordInfo(nsecRecord).RRSIGRecords;\n                        if (nsecRRSIGRecords is not null)\n                            nsecRecord.GetCacheRecordInfo().RRSIGRecords = nsecRRSIGRecords;\n                    }\n                }\n\n                if (resourceRecord.Type == DnsResourceRecordType.DNAME)\n                {\n                    if (dnameRecords is null)\n                        dnameRecords = new List<DnsResourceRecord>(1);\n\n                    dnameRecords.Add(resourceRecord);\n                }\n            }\n\n            if (resourceRecords.Count == 1)\n            {\n                DnsResourceRecord resourceRecord = resourceRecords[0];\n\n                CacheZone zone = _root.GetOrAdd(resourceRecord.Name, delegate (string key)\n                {\n                    return new CacheZone(resourceRecord.Name, 1);\n                });\n\n                if (zone.SetRecords(resourceRecord.Type, resourceRecords, _dnsServer.ServeStale))\n                    Interlocked.Increment(ref _totalEntries);\n            }\n            else\n            {\n                Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> groupedByDomainRecords = DnsResourceRecord.GroupRecords(resourceRecords);\n                bool serveStale = _dnsServer.ServeStale;\n\n                int addedEntries = 0;\n\n                //add grouped records\n                foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> groupedByTypeRecords in groupedByDomainRecords)\n                {\n                    if (dnameRecords is not null)\n                    {\n                        bool foundSynthesizedCNAME = false;\n\n                        foreach (DnsResourceRecord dnameRecord in dnameRecords)\n                        {\n                            if (groupedByTypeRecords.Key.EndsWith(\".\" + dnameRecord.Name, StringComparison.OrdinalIgnoreCase))\n                            {\n                                foundSynthesizedCNAME = true;\n                                break;\n                            }\n                        }\n\n                        if (foundSynthesizedCNAME)\n                            continue; //do not cache synthesized CNAME\n                    }\n\n                    CacheZone zone = _root.GetOrAdd(groupedByTypeRecords.Key, delegate (string key)\n                    {\n                        return new CacheZone(groupedByTypeRecords.Key, groupedByTypeRecords.Value.Count);\n                    });\n\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> groupedRecords in groupedByTypeRecords.Value)\n                    {\n                        if (zone.SetRecords(groupedRecords.Key, groupedRecords.Value, serveStale))\n                            addedEntries++;\n                    }\n                }\n\n                if (addedEntries > 0)\n                    Interlocked.Add(ref _totalEntries, addedEntries);\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private void CacheMaintenanceTimerCallback(object state)\n        {\n            try\n            {\n                RemoveExpiredRecords();\n\n                //force GC collection to remove old cache data from memory quickly\n                GC.Collect();\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n            finally\n            {\n                lock (_cacheMaintenanceTimerLock)\n                {\n                    _cacheMaintenanceTimer?.Change(CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);\n                }\n            }\n        }\n\n        private static IReadOnlyList<DnsResourceRecord> AddDSRecordsTo(CacheZone delegation, bool serveStale, IReadOnlyList<DnsResourceRecord> nsRecords, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet)\n        {\n            IReadOnlyList<DnsResourceRecord> records = delegation.QueryRecords(DnsResourceRecordType.DS, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n            if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.DS))\n            {\n                List<DnsResourceRecord> newNSRecords = new List<DnsResourceRecord>(nsRecords.Count + records.Count);\n\n                newNSRecords.AddRange(nsRecords);\n                newNSRecords.AddRange(records);\n\n                return newNSRecords;\n            }\n\n            //no DS records found check for NSEC records\n            IReadOnlyList<DnsResourceRecord> nsecRecords = nsRecords[0].GetCacheRecordInfo().NSECRecords;\n            if (nsecRecords is not null)\n            {\n                List<DnsResourceRecord> newNSRecords = new List<DnsResourceRecord>(nsRecords.Count + nsecRecords.Count);\n\n                newNSRecords.AddRange(nsRecords);\n                newNSRecords.AddRange(nsecRecords);\n\n                return newNSRecords;\n            }\n\n            //found nothing; return original NS records\n            return nsRecords;\n        }\n\n        private static void AddRRSIGRecords(IReadOnlyList<DnsResourceRecord> answer, out IReadOnlyList<DnsResourceRecord> newAnswer, out IReadOnlyList<DnsResourceRecord> newAuthority)\n        {\n            List<DnsResourceRecord> newAnswerList = new List<DnsResourceRecord>(answer.Count * 2);\n            List<DnsResourceRecord> newAuthorityList = null;\n\n            foreach (DnsResourceRecord record in answer)\n            {\n                if (record.Type == DnsResourceRecordType.RRSIG)\n                    continue; //skip RRSIG to avoid duplicates\n\n                newAnswerList.Add(record);\n\n                CacheRecordInfo rrInfo = record.GetCacheRecordInfo();\n\n                IReadOnlyList<DnsResourceRecord> rrsigRecords = rrInfo.RRSIGRecords;\n                if (rrsigRecords is not null)\n                {\n                    newAnswerList.AddRange(rrsigRecords);\n\n                    foreach (DnsResourceRecord rrsigRecord in rrsigRecords)\n                    {\n                        if (!DnsRRSIGRecordData.IsWildcard(rrsigRecord))\n                            continue;\n\n                        //add NSEC/NSEC3 for the wildcard proof\n                        if (newAuthorityList is null)\n                            newAuthorityList = new List<DnsResourceRecord>(2);\n\n                        IReadOnlyList<DnsResourceRecord> nsecRecords = rrInfo.NSECRecords;\n                        if (nsecRecords is not null)\n                        {\n                            foreach (DnsResourceRecord nsecRecord in nsecRecords)\n                            {\n                                newAuthorityList.Add(nsecRecord);\n\n                                IReadOnlyList<DnsResourceRecord> nsecRRSIGRecords = nsecRecord.GetCacheRecordInfo().RRSIGRecords;\n                                if (nsecRRSIGRecords is not null)\n                                    newAuthorityList.AddRange(nsecRRSIGRecords);\n                            }\n                        }\n                    }\n                }\n            }\n\n            newAnswer = newAnswerList;\n            newAuthority = newAuthorityList;\n        }\n\n        private void ResolveCNAME(DnsQuestionRecord question, DnsResourceRecord lastCNAME, bool serveStale, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, List<DnsResourceRecord> answerRecords)\n        {\n            int queryCount = 0;\n\n            do\n            {\n                string cnameDomain = (lastCNAME.RDATA as DnsCNAMERecordData).Domain;\n                if (lastCNAME.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase))\n                    break; //loop detected\n\n                if (!_root.TryGet(cnameDomain, out CacheZone cacheZone))\n                    break;\n\n                IReadOnlyList<DnsResourceRecord> records = cacheZone.QueryRecords(question.Type == DnsResourceRecordType.NS ? DnsResourceRecordType.CHILD_NS : question.Type, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n                if (records.Count < 1)\n                    break;\n\n                DnsResourceRecord lastRR = records[records.Count - 1];\n                if (lastRR.Type != DnsResourceRecordType.CNAME)\n                {\n                    answerRecords.AddRange(records);\n                    break;\n                }\n\n                foreach (DnsResourceRecord answerRecord in answerRecords)\n                {\n                    if (answerRecord.Type != DnsResourceRecordType.CNAME)\n                        continue;\n\n                    if (answerRecord.RDATA.Equals(lastRR.RDATA))\n                        return; //loop detected\n                }\n\n                answerRecords.AddRange(records);\n\n                lastCNAME = lastRR;\n            }\n            while (++queryCount < DnsServer.MAX_CNAME_HOPS);\n        }\n\n        private bool DoDNAMESubstitution(DnsQuestionRecord question, IReadOnlyList<DnsResourceRecord> answer, bool serveStale, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, out IReadOnlyList<DnsResourceRecord> newAnswer)\n        {\n            DnsResourceRecord dnameRR = answer[0];\n\n            string result = (dnameRR.RDATA as DnsDNAMERecordData).Substitute(question.Name, dnameRR.Name);\n\n            if (DnsClient.IsDomainNameValid(result))\n            {\n                DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TTL, new DnsCNAMERecordData(result));\n\n                List<DnsResourceRecord> list = new List<DnsResourceRecord>(5)\n                {\n                    dnameRR,\n                    cnameRR\n                };\n\n                ResolveCNAME(question, cnameRR, serveStale, eDnsClientSubnet, advancedForwardingClientSubnet, list);\n\n                newAnswer = list;\n                return true;\n            }\n            else\n            {\n                newAnswer = answer;\n                return false;\n            }\n        }\n\n        private List<DnsResourceRecord> GetAdditionalRecords(IReadOnlyList<DnsResourceRecord> refRecords, bool serveStale, bool dnssecOk, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet)\n        {\n            List<DnsResourceRecord> additionalRecords = new List<DnsResourceRecord>();\n\n            foreach (DnsResourceRecord refRecord in refRecords)\n            {\n                switch (refRecord.Type)\n                {\n                    case DnsResourceRecordType.NS:\n                        if (refRecord.RDATA is DnsNSRecordData ns)\n                            ResolveAdditionalRecords(refRecord, ns.NameServer, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords);\n\n                        break;\n\n                    case DnsResourceRecordType.MX:\n                        if (refRecord.RDATA is DnsMXRecordData mx)\n                            ResolveAdditionalRecords(refRecord, mx.Exchange, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords);\n\n                        break;\n\n                    case DnsResourceRecordType.SRV:\n                        if (refRecord.RDATA is DnsSRVRecordData srv)\n                            ResolveAdditionalRecords(refRecord, srv.Target, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords);\n\n                        break;\n\n                    case DnsResourceRecordType.SVCB:\n                    case DnsResourceRecordType.HTTPS:\n                        if (refRecord.RDATA is DnsSVCBRecordData svcb)\n                        {\n                            string targetName = svcb.TargetName;\n\n                            if (svcb.SvcPriority == 0)\n                            {\n                                //For AliasMode SVCB RRs, a TargetName of \".\" indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12]\n                                if ((targetName.Length == 0) || targetName.Equals(refRecord.Name, StringComparison.OrdinalIgnoreCase))\n                                    break;\n                            }\n                            else\n                            {\n                                //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]\n                                if (targetName.Length == 0)\n                                    targetName = refRecord.Name;\n                            }\n\n                            ResolveAdditionalRecords(refRecord, targetName, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords);\n                        }\n\n                        break;\n                }\n            }\n\n            return additionalRecords;\n        }\n\n        private void ResolveAdditionalRecords(DnsResourceRecord refRecord, string domain, bool serveStale, bool dnssecOk, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, List<DnsResourceRecord> additionalRecords)\n        {\n            IReadOnlyList<DnsResourceRecord> glueRecords = refRecord.GetCacheRecordInfo().GlueRecords;\n            if (glueRecords is not null)\n            {\n                bool added = false;\n\n                foreach (DnsResourceRecord glueRecord in glueRecords)\n                {\n                    if (!glueRecord.IsStale)\n                    {\n                        added = true;\n                        additionalRecords.Add(glueRecord);\n\n                        if (dnssecOk)\n                        {\n                            IReadOnlyList<DnsResourceRecord> rrsigRecords = glueRecord.GetCacheRecordInfo().RRSIGRecords;\n                            if (rrsigRecords is not null)\n                                additionalRecords.AddRange(rrsigRecords);\n                        }\n                    }\n                }\n\n                if (added)\n                    return;\n            }\n\n            int count = 0;\n\n            while ((count++ < DnsServer.MAX_CNAME_HOPS) && _root.TryGet(domain, out CacheZone cacheZone))\n            {\n                if (((refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) && ((refRecord.RDATA as DnsSVCBRecordData).SvcPriority == 0))\n                {\n                    //resolve SVCB/HTTPS for Alias mode refRecord\n                    IReadOnlyList<DnsResourceRecord> records = cacheZone.QueryRecords(refRecord.Type, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n                    if ((records.Count > 0) && (records[0].Type == refRecord.Type) && (records[0].RDATA is DnsSVCBRecordData svcb))\n                    {\n                        additionalRecords.AddRange(records);\n\n                        string targetName = svcb.TargetName;\n\n                        if (svcb.SvcPriority == 0)\n                        {\n                            //Alias mode\n                            if ((targetName.Length == 0) || targetName.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase))\n                                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]\n\n                            foreach (DnsResourceRecord additionalRecord in additionalRecords)\n                            {\n                                if (additionalRecord.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase))\n                                    return; //loop detected\n                            }\n\n                            //continue to resolve SVCB/HTTPS further\n                            domain = targetName;\n                            refRecord = records[0];\n                            continue;\n                        }\n                        else\n                        {\n                            //Service mode\n                            if (targetName.Length > 0)\n                            {\n                                //continue to resolve A/AAAA for target name\n                                domain = targetName;\n                                refRecord = records[0];\n                                continue;\n                            }\n\n                            //resolve A/AAAA below\n                        }\n                    }\n                }\n\n                bool hasA = false;\n                bool hasAAAA = false;\n\n                if ((refRecord.Type == DnsResourceRecordType.SRV) || (refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS))\n                {\n                    foreach (DnsResourceRecord additionalRecord in additionalRecords)\n                    {\n                        if (additionalRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                        {\n                            switch (additionalRecord.Type)\n                            {\n                                case DnsResourceRecordType.A:\n                                    hasA = true;\n                                    break;\n\n                                case DnsResourceRecordType.AAAA:\n                                    hasAAAA = true;\n                                    break;\n                            }\n                        }\n\n                        if (hasA && hasAAAA)\n                            break;\n                    }\n                }\n\n                if (!hasA)\n                {\n                    IReadOnlyList<DnsResourceRecord> records = cacheZone.QueryRecords(DnsResourceRecordType.A, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n                    if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.A))\n                        additionalRecords.AddRange(records);\n                }\n\n                if (!hasAAAA)\n                {\n                    IReadOnlyList<DnsResourceRecord> records = cacheZone.QueryRecords(DnsResourceRecordType.AAAA, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n                    if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.AAAA))\n                        additionalRecords.AddRange(records);\n                }\n\n                break;\n            }\n        }\n\n        private int RemoveExpiredRecordsInternal(bool serveStale, long minimumEntriesToRemove)\n        {\n            int removedEntries = 0;\n\n            foreach (CacheZone zone in _root)\n            {\n                removedEntries += zone.RemoveExpiredRecords(serveStale);\n\n                if (zone.IsEmpty)\n                    _root.TryRemove(zone.Name, out _); //remove empty zone\n\n                if ((minimumEntriesToRemove > 0) && (removedEntries >= minimumEntriesToRemove))\n                    break;\n            }\n\n            if (removedEntries > 0)\n            {\n                long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries);\n                if (totalEntries < 0)\n                    Interlocked.Add(ref _totalEntries, -totalEntries);\n            }\n\n            return removedEntries;\n        }\n\n        private int RemoveLeastUsedRecordsInternal(DateTime cutoff, long minimumEntriesToRemove)\n        {\n            int removedEntries = 0;\n\n            foreach (CacheZone zone in _root)\n            {\n                removedEntries += zone.RemoveLeastUsedRecords(cutoff);\n\n                if (zone.IsEmpty)\n                    _root.TryRemove(zone.Name, out _); //remove empty zone\n\n                if ((minimumEntriesToRemove > 0) && (removedEntries >= minimumEntriesToRemove))\n                    break;\n            }\n\n            if (removedEntries > 0)\n            {\n                long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries);\n                if (totalEntries < 0)\n                    Interlocked.Add(ref _totalEntries, -totalEntries);\n            }\n\n            return removedEntries;\n        }\n\n        #endregion\n\n        #region public\n\n        public override void RemoveExpiredRecords()\n        {\n            bool serveStale = _dnsServer.ServeStale;\n\n            //remove expired records/expired stale records\n            RemoveExpiredRecordsInternal(serveStale, 0);\n\n            if (_maximumEntries < 1)\n                return; //cache limit feature disabled\n\n            //find minimum entries to remove\n            long minimumEntriesToRemove = _totalEntries - _maximumEntries;\n            if (minimumEntriesToRemove < 1)\n                return; //no need to remove\n\n            //remove stale records if they exist\n            if (serveStale)\n                minimumEntriesToRemove -= RemoveExpiredRecordsInternal(false, minimumEntriesToRemove);\n\n            if (minimumEntriesToRemove < 1)\n                return; //task completed\n\n            //remove least recently used records\n            for (int seconds = 86400; seconds > 0; seconds /= 2)\n            {\n                DateTime cutoff = DateTime.UtcNow.AddSeconds(-seconds);\n\n                minimumEntriesToRemove -= RemoveLeastUsedRecordsInternal(cutoff, minimumEntriesToRemove);\n\n                if (minimumEntriesToRemove < 1)\n                    break; //task completed\n            }\n        }\n\n        public void DeleteEDnsClientSubnetData()\n        {\n            int removedEntries = 0;\n\n            foreach (CacheZone zone in _root)\n            {\n                removedEntries += zone.DeleteEDnsClientSubnetData();\n\n                if (zone.IsEmpty)\n                    _root.TryRemove(zone.Name, out _); //remove empty zone\n            }\n\n            if (removedEntries > 0)\n            {\n                long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries);\n                if (totalEntries < 0)\n                    Interlocked.Add(ref _totalEntries, -totalEntries);\n            }\n        }\n\n        public override void Flush()\n        {\n            _root.Clear();\n\n            long totalEntries = _totalEntries;\n            totalEntries = Interlocked.Add(ref _totalEntries, -totalEntries);\n            if (totalEntries < 0)\n                Interlocked.Add(ref _totalEntries, -totalEntries);\n        }\n\n        public bool DeleteZone(string domain)\n        {\n            if (_root.TryRemoveTree(domain, out _, out int removedEntries))\n            {\n                if (removedEntries > 0)\n                {\n                    long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries);\n                    if (totalEntries < 0)\n                        Interlocked.Add(ref _totalEntries, -totalEntries);\n                }\n\n                return true;\n            }\n\n            return false;\n        }\n\n        public void ListSubDomains(string domain, List<string> subDomains)\n        {\n            _root.ListSubDomains(domain, subDomains);\n        }\n\n        public void ListAllRecords(string domain, List<DnsResourceRecord> records)\n        {\n            if (_root.TryGet(domain, out CacheZone zone))\n                zone.ListAllRecords(records);\n        }\n\n        public Task<DnsDatagram> QueryClosestDelegationAsync(DnsDatagram request)\n        {\n            DnsQuestionRecord question = request.Question[0];\n            string domain = question.Name;\n\n            NetworkAddress eDnsClientSubnet = null;\n            bool advancedForwardingClientSubnet = false;\n            {\n                EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                if (requestECS is not null)\n                {\n                    eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength);\n                    advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet;\n                }\n            }\n\n            if (question.Type == DnsResourceRecordType.DS)\n            {\n                //find parent delegation\n                domain = AuthZoneManager.GetParentZone(question.Name);\n                if (domain is null)\n                    return Task.FromResult<DnsDatagram>(null); //dont find NS for root\n            }\n\n            do\n            {\n                _ = _root.FindZone(domain, out _, out CacheZone delegation);\n                if (delegation is null)\n                    return Task.FromResult<DnsDatagram>(null);\n\n                //return closest name servers in delegation\n                IReadOnlyList<DnsResourceRecord> closestAuthority = delegation.QueryRecords(DnsResourceRecordType.NS, false, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n                if ((closestAuthority.Count == 0) && (delegation.Name.Length == 0))\n                    closestAuthority = delegation.QueryRecords(DnsResourceRecordType.CHILD_NS, false, true, eDnsClientSubnet, advancedForwardingClientSubnet); //root zone case\n\n                if ((closestAuthority.Count > 0) && (closestAuthority[0].Type == DnsResourceRecordType.NS))\n                {\n                    if (request.DnssecOk)\n                    {\n                        if (closestAuthority[0].DnssecStatus != DnssecStatus.Disabled) //dont return records with disabled status\n                        {\n                            closestAuthority = AddDSRecordsTo(delegation, false, closestAuthority, eDnsClientSubnet, advancedForwardingClientSubnet);\n\n                            IReadOnlyList<DnsResourceRecord> additional = GetAdditionalRecords(closestAuthority, false, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n\n                            return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, closestAuthority, additional));\n                        }\n                    }\n                    else\n                    {\n                        IReadOnlyList<DnsResourceRecord> additional = GetAdditionalRecords(closestAuthority, false, false, eDnsClientSubnet, advancedForwardingClientSubnet);\n\n                        return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, closestAuthority, additional));\n                    }\n                }\n\n                domain = AuthZoneManager.GetParentZone(delegation.Name);\n            }\n            while (domain is not null);\n\n            //no cached delegation found\n            return Task.FromResult<DnsDatagram>(null);\n        }\n\n        public override Task<DnsDatagram> QueryAsync(DnsDatagram request, bool serveStale = false, bool findClosestNameServers = false, bool resetExpiry = false)\n        {\n            DnsQuestionRecord question = request.Question[0];\n\n            NetworkAddress eDnsClientSubnet = null;\n            bool advancedForwardingClientSubnet = false;\n            {\n                EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption();\n                if (requestECS is not null)\n                {\n                    eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength);\n                    advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet;\n                }\n            }\n\n            CacheZone zone;\n            CacheZone closest = null;\n            CacheZone delegation = null;\n\n            if (findClosestNameServers)\n            {\n                zone = _root.FindZone(question.Name, out closest, out delegation);\n            }\n            else\n            {\n                if (!_root.TryGet(question.Name, out zone))\n                    _ = _root.FindZone(question.Name, out closest, out _); //zone not found; attempt to find closest\n            }\n\n            bool dnssecOk = request.DnssecOk;\n\n            if (zone is not null)\n            {\n                //zone found\n                IReadOnlyList<DnsResourceRecord> answer = zone.QueryRecords(question.Type == DnsResourceRecordType.NS ? DnsResourceRecordType.CHILD_NS : question.Type, serveStale, false, eDnsClientSubnet, advancedForwardingClientSubnet);\n                if (answer.Count > 0)\n                {\n                    //answer found in cache\n                    DnsResourceRecord firstRR = answer[0];\n\n                    if (firstRR.RDATA is DnsSpecialCacheRecordData dnsSpecialCacheRecord)\n                    {\n                        if (dnssecOk)\n                        {\n                            foreach (DnsResourceRecord originalAuthority in dnsSpecialCacheRecord.OriginalAuthority)\n                            {\n                                if (originalAuthority.DnssecStatus == DnssecStatus.Disabled)\n                                    goto beforeFindClosestNameServers; //dont return answer with disabled status\n                            }\n                        }\n\n                        if (resetExpiry)\n                        {\n                            if (firstRR.IsStale)\n                                firstRR.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767\n\n                            if (dnsSpecialCacheRecord.Authority is not null)\n                            {\n                                foreach (DnsResourceRecord record in dnsSpecialCacheRecord.Authority)\n                                {\n                                    if (record.IsStale)\n                                        record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767\n                                }\n                            }\n                        }\n\n                        IReadOnlyList<EDnsOption> specialOptions;\n\n                        if (firstRR.WasExpiryReset || firstRR.IsStale)\n                        {\n                            List<EDnsOption> newOptions = new List<EDnsOption>(dnsSpecialCacheRecord.EDnsOptions.Count + 1);\n\n                            newOptions.AddRange(dnsSpecialCacheRecord.EDnsOptions);\n\n                            if (dnsSpecialCacheRecord.RCODE == DnsResponseCode.NxDomain)\n                                newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleNxDomainAnswer, firstRR.Name.ToLowerInvariant() + \" \" + firstRR.Type.ToString() + \" \" + firstRR.Class.ToString())));\n                            else\n                                newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleAnswer, firstRR.Name.ToLowerInvariant() + \" \" + firstRR.Type.ToString() + \" \" + firstRR.Class.ToString())));\n\n                            specialOptions = newOptions;\n                        }\n                        else\n                        {\n                            specialOptions = dnsSpecialCacheRecord.EDnsOptions;\n                        }\n\n                        if (eDnsClientSubnet is not null)\n                        {\n                            EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true);\n                            if (requestECS is not null)\n                            {\n                                NetworkAddress recordECS = firstRR.GetCacheRecordInfo().EDnsClientSubnet;\n                                if (recordECS is not null)\n                                {\n                                    EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, recordECS.PrefixLength, requestECS.Address);\n\n                                    if ((specialOptions is null) || (specialOptions.Count == 0))\n                                    {\n                                        specialOptions = ecsOption;\n                                    }\n                                    else\n                                    {\n                                        List<EDnsOption> newOptions = new List<EDnsOption>(specialOptions.Count + 1);\n\n                                        newOptions.AddRange(specialOptions);\n                                        newOptions.Add(ecsOption[0]);\n\n                                        specialOptions = newOptions;\n                                    }\n                                }\n                            }\n                        }\n\n                        if (dnssecOk)\n                        {\n                            bool authenticData;\n\n                            switch (dnsSpecialCacheRecord.Type)\n                            {\n                                case DnsSpecialCacheRecordType.NegativeCache:\n                                    authenticData = true;\n                                    break;\n\n                                default:\n                                    authenticData = false;\n                                    break;\n                            }\n\n                            if (request.CheckingDisabled)\n                                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));\n                            else\n                                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));\n                        }\n                        else\n                        {\n                            if (request.CheckingDisabled)\n                                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));\n                            else\n                                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));\n                        }\n                    }\n\n                    DnsResourceRecord lastRR = answer[answer.Count - 1];\n                    if ((lastRR.Type != question.Type) && (lastRR.Type == DnsResourceRecordType.CNAME) && (question.Type != DnsResourceRecordType.ANY))\n                    {\n                        List<DnsResourceRecord> newAnswers = new List<DnsResourceRecord>(answer.Count + 3);\n                        newAnswers.AddRange(answer);\n\n                        ResolveCNAME(question, lastRR, serveStale, eDnsClientSubnet, advancedForwardingClientSubnet, newAnswers);\n\n                        answer = newAnswers;\n                    }\n\n                    IReadOnlyList<DnsResourceRecord> authority = null;\n                    EDnsHeaderFlags ednsFlags = EDnsHeaderFlags.None;\n\n                    if (dnssecOk)\n                    {\n                        //DNSSEC enabled\n                        foreach (DnsResourceRecord record in answer)\n                        {\n                            if (record.DnssecStatus == DnssecStatus.Disabled)\n                                goto beforeFindClosestNameServers; //dont return answer when status is disabled\n                        }\n\n                        //add RRSIG records\n                        AddRRSIGRecords(answer, out answer, out authority);\n\n                        ednsFlags = EDnsHeaderFlags.DNSSEC_OK;\n                    }\n\n                    IReadOnlyList<DnsResourceRecord> additional = null;\n\n                    switch (question.Type)\n                    {\n                        case DnsResourceRecordType.NS:\n                        case DnsResourceRecordType.MX:\n                        case DnsResourceRecordType.SRV:\n                        case DnsResourceRecordType.SVCB:\n                        case DnsResourceRecordType.HTTPS:\n                            additional = GetAdditionalRecords(answer, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet);\n                            break;\n                    }\n\n                    if (resetExpiry)\n                    {\n                        foreach (DnsResourceRecord record in answer)\n                        {\n                            if (record.IsStale)\n                                record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767\n                        }\n\n                        if (additional is not null)\n                        {\n                            foreach (DnsResourceRecord record in additional)\n                            {\n                                if (record.IsStale)\n                                    record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767\n                            }\n                        }\n                    }\n\n                    IReadOnlyList<EDnsOption> options = null;\n\n                    foreach (DnsResourceRecord record in answer)\n                    {\n                        if (record.WasExpiryReset || record.IsStale)\n                            options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleAnswer, record.Name.ToLowerInvariant() + \" \" + record.Type.ToString() + \" \" + record.Class.ToString()))];\n                    }\n\n                    if (eDnsClientSubnet is not null)\n                    {\n                        EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true);\n                        if (requestECS is not null)\n                        {\n                            NetworkAddress suitableECS = null;\n\n                            foreach (DnsResourceRecord record in answer)\n                            {\n                                NetworkAddress recordECS = record.GetCacheRecordInfo().EDnsClientSubnet;\n                                if (recordECS is not null)\n                                {\n                                    if ((suitableECS is null) || (recordECS.PrefixLength > suitableECS.PrefixLength))\n                                        suitableECS = recordECS;\n                                }\n                            }\n\n                            if (suitableECS is not null)\n                            {\n                                EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, suitableECS.PrefixLength, requestECS.Address);\n\n                                if (options is null)\n                                {\n                                    options = ecsOption;\n                                }\n                                else\n                                {\n                                    List<EDnsOption> newOptions = new List<EDnsOption>(options.Count + 1);\n\n                                    newOptions.AddRange(options);\n                                    newOptions.Add(ecsOption[0]);\n\n                                    options = newOptions;\n                                }\n                            }\n                        }\n                    }\n\n                    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));\n                }\n            }\n            else\n            {\n                //zone not found\n                //check for DNAME in closest zone\n                if (closest is not null)\n                {\n                    IReadOnlyList<DnsResourceRecord> answer = closest.QueryRecords(DnsResourceRecordType.DNAME, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n                    if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME))\n                    {\n                        DnsResponseCode rCode;\n\n                        if (DoDNAMESubstitution(question, answer, serveStale, eDnsClientSubnet, advancedForwardingClientSubnet, out answer))\n                            rCode = DnsResponseCode.NoError;\n                        else\n                            rCode = DnsResponseCode.YXDomain;\n\n                        IReadOnlyList<DnsResourceRecord> authority = null;\n                        EDnsHeaderFlags ednsFlags = EDnsHeaderFlags.None;\n\n                        if (dnssecOk)\n                        {\n                            //DNSSEC enabled\n                            foreach (DnsResourceRecord record in answer)\n                            {\n                                if (record.DnssecStatus == DnssecStatus.Disabled)\n                                    goto beforeFindClosestNameServers; //dont return answer when status is disabled\n                            }\n\n                            //add RRSIG records\n                            AddRRSIGRecords(answer, out answer, out authority);\n\n                            ednsFlags = EDnsHeaderFlags.DNSSEC_OK;\n                        }\n\n                        if (resetExpiry)\n                        {\n                            foreach (DnsResourceRecord record in answer)\n                            {\n                                if (record.IsStale)\n                                    record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767\n                            }\n                        }\n\n                        EDnsOption[] options = null;\n\n                        foreach (DnsResourceRecord record in answer)\n                        {\n                            if (record.WasExpiryReset || record.IsStale)\n                                options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleAnswer, record.Name.ToLowerInvariant() + \" \" + record.Type.ToString() + \" \" + record.Class.ToString()))];\n                        }\n\n                        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));\n                    }\n                }\n            }\n\n        //no answer in cache\n        beforeFindClosestNameServers:\n\n            //check for closest delegation if any\n            if (findClosestNameServers && (delegation is not null))\n            {\n                //return closest name servers in delegation\n                if (question.Type == DnsResourceRecordType.DS)\n                {\n                    //find parent delegation\n                    string domain = AuthZoneManager.GetParentZone(question.Name);\n                    if (domain is null)\n                        return Task.FromResult<DnsDatagram>(null); //dont find NS for root\n\n                    _ = _root.FindZone(domain, out _, out delegation);\n                    if (delegation is null)\n                        return Task.FromResult<DnsDatagram>(null); //no cached delegation found\n                }\n\n                while (true)\n                {\n                    IReadOnlyList<DnsResourceRecord> closestAuthority = delegation.QueryRecords(DnsResourceRecordType.NS, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n                    if ((closestAuthority.Count == 0) && (delegation.Name.Length == 0))\n                        closestAuthority = delegation.QueryRecords(DnsResourceRecordType.CHILD_NS, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); //root zone case\n\n                    if ((closestAuthority.Count > 0) && (closestAuthority[0].Type == DnsResourceRecordType.NS))\n                    {\n                        if (dnssecOk)\n                        {\n                            if (closestAuthority[0].DnssecStatus != DnssecStatus.Disabled) //dont return records with disabled status\n                            {\n                                closestAuthority = AddDSRecordsTo(delegation, serveStale, closestAuthority, eDnsClientSubnet, advancedForwardingClientSubnet);\n\n                                IReadOnlyList<DnsResourceRecord> additional = GetAdditionalRecords(closestAuthority, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet);\n\n                                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));\n                            }\n                        }\n                        else\n                        {\n                            IReadOnlyList<DnsResourceRecord> additional = GetAdditionalRecords(closestAuthority, serveStale, false, eDnsClientSubnet, advancedForwardingClientSubnet);\n\n                            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));\n                        }\n                    }\n\n                    string domain = AuthZoneManager.GetParentZone(delegation.Name);\n                    if (domain is null)\n                        return Task.FromResult<DnsDatagram>(null); //dont find NS for root\n\n                    _ = _root.FindZone(domain, out _, out delegation);\n                    if (delegation is null)\n                        return Task.FromResult<DnsDatagram>(null); //no cached delegation found\n                }\n            }\n\n            //no cached delegation found\n            return Task.FromResult<DnsDatagram>(null);\n        }\n\n        #endregion\n\n        #region properties\n\n        public uint ServeStaleResetTtl\n        {\n            get { return _serveStaleResetTtl; }\n            set\n            {\n                if ((value < SERVE_STALE_MIN_RESET_TTL) || (value > SERVE_STALE_MAX_RESET_TTL))\n                    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.\");\n\n                _serveStaleResetTtl = value;\n            }\n        }\n\n        public long MaximumEntries\n        {\n            get { return _maximumEntries; }\n            set\n            {\n                if (value < 0)\n                    throw new ArgumentOutOfRangeException(nameof(MaximumEntries), \"Invalid cache maximum entries value. Valid range is 0 and above.\");\n\n                _maximumEntries = value;\n            }\n        }\n\n        public long TotalEntries\n        { get { return _totalEntries; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/ApexZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    public enum AuthZoneQueryAccess : byte\n    {\n        Deny = 0,\n        Allow = 1,\n        AllowOnlyPrivateNetworks = 2,\n        AllowOnlyZoneNameServers = 3,\n        UseSpecifiedNetworkACL = 4,\n        AllowZoneNameServersAndUseSpecifiedNetworkACL = 5\n    }\n\n    public enum AuthZoneTransfer : byte\n    {\n        Deny = 0,\n        Allow = 1,\n        AllowOnlyZoneNameServers = 2,\n        UseSpecifiedNetworkACL = 3,\n        AllowZoneNameServersAndUseSpecifiedNetworkACL = 4\n    }\n\n    public enum AuthZoneNotify : byte\n    {\n        None = 0,\n        ZoneNameServers = 1,\n        SpecifiedNameServers = 2,\n        BothZoneAndSpecifiedNameServers = 3,\n        SeparateNameServersForCatalogAndMemberZones = 4\n    }\n\n    public enum AuthZoneUpdate : byte\n    {\n        Deny = 0,\n        Allow = 1,\n        AllowOnlyZoneNameServers = 2,\n        UseSpecifiedNetworkACL = 3,\n        AllowZoneNameServersAndUseSpecifiedNetworkACL = 4\n    }\n\n    abstract class ApexZone : AuthZone, IDisposable\n    {\n        #region variables\n\n        protected readonly DnsServer _dnsServer;\n        protected DateTime _lastModified;\n\n        string _catalogZoneName;\n        bool _overrideCatalogQueryAccess;\n        bool _overrideCatalogZoneTransfer;\n        bool _overrideCatalogNotify;\n\n        protected AuthZoneQueryAccess _queryAccess;\n        IReadOnlyCollection<NetworkAccessControl> _queryAccessNetworkACL;\n\n        protected AuthZoneTransfer _zoneTransfer;\n        IReadOnlyCollection<NetworkAccessControl> _zoneTransferNetworkACL;\n        IReadOnlySet<string> _zoneTransferTsigKeyNames;\n        readonly List<DnsResourceRecord> _zoneHistory; //for IXFR support\n\n        AuthZoneNotify _notify;\n        IReadOnlyCollection<IPAddress> _notifyNameServers;\n        IReadOnlyCollection<IPAddress> _notifySecondaryCatalogNameServers;\n\n        AuthZoneUpdate _update;\n        IReadOnlyCollection<NetworkAccessControl> _updateNetworkACL;\n        IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> _updateSecurityPolicies;\n\n        protected AuthZoneDnssecStatus _dnssecStatus;\n\n        Timer _notifyTimer;\n        bool _notifyTimerTriggered;\n        const int NOTIFY_TIMER_INTERVAL = 5000;\n        List<string> _notifyList;\n        List<string> _notifyFailed;\n        const int NOTIFY_TIMEOUT = 10000;\n        const int NOTIFY_RETRIES = 5;\n\n        protected bool _syncFailed;\n\n        Timer _recordExpiryTimer;\n        readonly object _recordExpiryTimerLock = new object();\n        DateTime _recordExpiryTimerStartedOn;\n        uint _recordExpiryTimerTtl;\n        bool _recordExpiryTimerRunning;\n\n        CatalogZone _catalogZone;\n        SecondaryCatalogZone _secondaryCatalogZone;\n\n        #endregion\n\n        #region constructor\n\n        protected ApexZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(zoneInfo)\n        {\n            _dnsServer = dnsServer;\n\n            _catalogZoneName = zoneInfo.CatalogZoneName;\n            _overrideCatalogQueryAccess = zoneInfo.OverrideCatalogQueryAccess;\n            _overrideCatalogZoneTransfer = zoneInfo.OverrideCatalogZoneTransfer;\n            _overrideCatalogNotify = zoneInfo.OverrideCatalogNotify;\n\n            _queryAccess = zoneInfo.QueryAccess;\n            _queryAccessNetworkACL = zoneInfo.QueryAccessNetworkACL;\n\n            _zoneTransfer = zoneInfo.ZoneTransfer;\n            _zoneTransferNetworkACL = zoneInfo.ZoneTransferNetworkACL;\n            _zoneTransferTsigKeyNames = zoneInfo.ZoneTransferTsigKeyNames;\n\n            if (zoneInfo.ZoneHistory is null)\n                _zoneHistory = new List<DnsResourceRecord>();\n            else\n                _zoneHistory = new List<DnsResourceRecord>(zoneInfo.ZoneHistory);\n\n            _notify = zoneInfo.Notify;\n            _notifyNameServers = zoneInfo.NotifyNameServers;\n            _notifySecondaryCatalogNameServers = zoneInfo.NotifySecondaryCatalogNameServers;\n\n            _update = zoneInfo.Update;\n            _updateNetworkACL = zoneInfo.UpdateNetworkACL;\n            _updateSecurityPolicies = zoneInfo.UpdateSecurityPolicies;\n\n            _lastModified = zoneInfo.LastModified;\n        }\n\n        protected ApexZone(DnsServer dnsServer, string name)\n            : base(name)\n        {\n            _dnsServer = dnsServer;\n\n            _queryAccess = AuthZoneQueryAccess.Allow;\n            _zoneHistory = new List<DnsResourceRecord>();\n\n            _lastModified = DateTime.UtcNow;\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (_disposed)\n                return;\n\n            if (disposing)\n            {\n                _notifyTimer?.Dispose();\n\n                lock (_recordExpiryTimerLock)\n                {\n                    if (_recordExpiryTimer is not null)\n                    {\n                        _recordExpiryTimer.Dispose();\n                        _recordExpiryTimer = null;\n                    }\n                }\n            }\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n        }\n\n        #endregion\n\n        #region notify\n\n        protected void InitNotify()\n        {\n            _notifyTimer = new Timer(NotifyTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n            _notifyList = new List<string>();\n            _notifyFailed = new List<string>();\n        }\n\n        protected void DisableNotifyTimer()\n        {\n            if (_notifyTimer is not null)\n                _notifyTimer.Change(Timeout.Infinite, Timeout.Infinite);\n        }\n\n        private void NotifyTimerCallback(object state)\n        {\n            ApexZone apexZone = this;\n\n            if ((apexZone.CatalogZone is not null) && !apexZone.OverrideCatalogNotify)\n                apexZone = apexZone.CatalogZone;\n\n            List<string> notifiedNameServers = new List<string>();\n\n            async Task NotifyZoneNameServersAsync(bool onlyFailedNameServers)\n            {\n                string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer;\n                IReadOnlyList<DnsResourceRecord> nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords\n\n                //notify all secondary name servers\n                List<Task> tasks = new List<Task>();\n\n                foreach (DnsResourceRecord nsRecord in nsRecords)\n                {\n                    if (nsRecord.GetAuthGenericRecordInfo().Disabled)\n                        continue;\n\n                    string nameServerHost = (nsRecord.RDATA as DnsNSRecordData).NameServer;\n\n                    if (primaryNameServer.Equals(nameServerHost, StringComparison.OrdinalIgnoreCase))\n                        continue; //skip primary name server\n\n                    if (onlyFailedNameServers)\n                    {\n                        lock (_notifyFailed)\n                        {\n                            if (!_notifyFailed.Contains(nameServerHost))\n                                continue;\n                        }\n                    }\n\n                    notifiedNameServers.Add(nameServerHost);\n\n                    List<NameServerAddress> nameServers = new List<NameServerAddress>(2);\n                    await ResolveNameServerAddressesAsync(nsRecord, nameServers);\n\n                    if (nameServers.Count > 0)\n                    {\n                        tasks.Add(NotifyNameServerAsync(nameServerHost, nameServers));\n                    }\n                    else\n                    {\n                        lock (_notifyFailed)\n                        {\n                            if (!_notifyFailed.Contains(nameServerHost))\n                                _notifyFailed.Add(nameServerHost);\n                        }\n\n                        _dnsServer.LogManager.Write(\"DNS Server failed to notify name server '\" + nameServerHost + \"' due to failure in resolving its IP address for zone: \" + ToString());\n                    }\n                }\n\n                await Task.WhenAll(tasks);\n            }\n\n            Task NotifySpecifiedNameServersAsync(bool onlyFailedNameServers)\n            {\n                IReadOnlyCollection<IPAddress> specifiedNameServers = apexZone._notifyNameServers;\n                if (specifiedNameServers is not null)\n                    return NotifyNameServersAsync(specifiedNameServers, onlyFailedNameServers);\n\n                return Task.CompletedTask;\n            }\n\n            Task NotifySecondaryCatalogNameServersAsync(bool onlyFailedNameServers)\n            {\n                IReadOnlyCollection<IPAddress> secondaryCatalogNameServers = apexZone._notifySecondaryCatalogNameServers;\n                if (secondaryCatalogNameServers is not null)\n                    return NotifyNameServersAsync(secondaryCatalogNameServers, onlyFailedNameServers);\n\n                return Task.CompletedTask;\n            }\n\n            async Task NotifyNameServersAsync(IReadOnlyCollection<IPAddress> nameServerIpAddresses, bool onlyFailedNameServers)\n            {\n                List<Task> tasks = new List<Task>();\n\n                foreach (IPAddress nameServerIpAddress in nameServerIpAddresses)\n                {\n                    string nameServerHost = nameServerIpAddress.ToString();\n\n                    if (onlyFailedNameServers)\n                    {\n                        lock (_notifyFailed)\n                        {\n                            if (!_notifyFailed.Contains(nameServerHost))\n                                continue;\n                        }\n                    }\n\n                    notifiedNameServers.Add(nameServerHost);\n\n                    tasks.Add(NotifyNameServerAsync(nameServerHost, [new NameServerAddress(nameServerIpAddress)]));\n                }\n\n                await Task.WhenAll(tasks);\n            }\n\n            //notify in DNS server's resolver thread pool\n            if (!_dnsServer.TryQueueResolverTask(async delegate (object state)\n                {\n                    try\n                    {\n                        switch (apexZone._notify)\n                        {\n                            case AuthZoneNotify.ZoneNameServers:\n                                await NotifyZoneNameServersAsync(!_notifyTimerTriggered);\n                                break;\n\n                            case AuthZoneNotify.SpecifiedNameServers:\n                                await NotifySpecifiedNameServersAsync(!_notifyTimerTriggered);\n                                break;\n\n                            case AuthZoneNotify.BothZoneAndSpecifiedNameServers:\n                                Task t1 = NotifyZoneNameServersAsync(!_notifyTimerTriggered);\n                                Task t2 = NotifySpecifiedNameServersAsync(!_notifyTimerTriggered);\n\n                                await Task.WhenAll(t1, t2);\n                                break;\n\n                            case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones:\n                                if (this is CatalogZone)\n                                    await NotifySecondaryCatalogNameServersAsync(!_notifyTimerTriggered);\n                                else\n                                    await NotifySpecifiedNameServersAsync(!_notifyTimerTriggered);\n\n                                break;\n                        }\n\n                        //remove non-existent name servers from notify failed list\n                        lock (_notifyFailed)\n                        {\n                            if (_notifyFailed.Count > 0)\n                            {\n                                List<string> toRemove = new List<string>();\n\n                                foreach (string failedNameServer in _notifyFailed)\n                                {\n                                    if (!notifiedNameServers.Contains(failedNameServer))\n                                        toRemove.Add(failedNameServer);\n                                }\n\n                                foreach (string failedNameServer in toRemove)\n                                    _notifyFailed.Remove(failedNameServer);\n\n                                if (_notifyFailed.Count > 0)\n                                {\n                                    //set timer to notify failed name servers again\n                                    _notifyTimer?.Change(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000, Timeout.Infinite);\n                                }\n                            }\n                        }\n                    }\n                    catch (ObjectDisposedException)\n                    { }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                    finally\n                    {\n                        _notifyTimerTriggered = false;\n                    }\n                })\n            )\n            {\n                //failed to queue notify task; try again in some time\n                try\n                {\n                    _notifyTimer?.Change(NOTIFY_TIMER_INTERVAL, Timeout.Infinite);\n                }\n                catch (ObjectDisposedException)\n                { }\n            }\n        }\n\n        private async Task NotifyNameServerAsync(string nameServerHost, IReadOnlyList<NameServerAddress> nameServers)\n        {\n            //use notify list to prevent multiple threads from notifying the same name server\n            lock (_notifyList)\n            {\n                if (_notifyList.Contains(nameServerHost))\n                    return; //already notifying the name server in another thread\n\n                _notifyList.Add(nameServerHost);\n            }\n\n            try\n            {\n                DnsClient client = new DnsClient(nameServers);\n\n                client.Proxy = _dnsServer.Proxy;\n                client.Timeout = NOTIFY_TIMEOUT;\n                client.Retries = NOTIFY_RETRIES;\n\n                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]);\n                DnsDatagram response = await client.RawResolveAsync(notifyRequest);\n\n                switch (response.RCODE)\n                {\n                    case DnsResponseCode.NoError:\n                    case DnsResponseCode.NotImplemented:\n                        {\n                            //transaction complete\n                            lock (_notifyFailed)\n                            {\n                                _notifyFailed.Remove(nameServerHost);\n                            }\n\n                            _dnsServer.LogManager.Write(\"DNS Server successfully notified name server '\" + nameServerHost + \"' for zone: \" + ToString());\n                        }\n                        break;\n\n                    default:\n                        {\n                            //transaction failed\n                            lock (_notifyFailed)\n                            {\n                                if (!_notifyFailed.Contains(nameServerHost))\n                                    _notifyFailed.Add(nameServerHost);\n                            }\n\n                            _dnsServer.LogManager.Write(\"DNS Server failed to notify name server '\" + nameServerHost + \"' (RCODE=\" + response.RCODE.ToString() + \") for zone: \" + ToString());\n                        }\n                        break;\n                }\n            }\n            catch (Exception ex)\n            {\n                lock (_notifyFailed)\n                {\n                    if (!_notifyFailed.Contains(nameServerHost))\n                        _notifyFailed.Add(nameServerHost);\n                }\n\n                _dnsServer.LogManager.Write(\"DNS Server failed to notify name server '\" + nameServerHost + \"' for zone: \" + ToString() + \"\\r\\n\" + ex.ToString());\n            }\n            finally\n            {\n                lock (_notifyList)\n                {\n                    _notifyList.Remove(nameServerHost);\n                }\n            }\n        }\n\n        internal void RemoveFromNotifyFailedList(NameServerAddress allowedZoneNameServer, IPAddress allowedIPAddress)\n        {\n            if (_notifyFailed is null)\n                return;\n\n            lock (_notifyFailed)\n            {\n                if (_notifyFailed.Count == 0)\n                    return;\n\n                if ((allowedZoneNameServer is not null) && (allowedZoneNameServer.DomainEndPoint is not null))\n                    _notifyFailed.Remove(allowedZoneNameServer.DomainEndPoint.Address);\n\n                _notifyFailed.Remove(allowedIPAddress.ToString());\n            }\n        }\n\n        public void TriggerNotify()\n        {\n            if (Disabled)\n                return;\n\n            ApexZone apexZone = this;\n\n            if ((apexZone.CatalogZone is not null) && !apexZone.OverrideCatalogNotify)\n                apexZone = apexZone.CatalogZone;\n\n            if (apexZone._notify == AuthZoneNotify.None)\n            {\n                if (_notifyFailed is not null)\n                {\n                    lock (_notifyFailed)\n                    {\n                        _notifyFailed.Clear();\n                    }\n                }\n\n                return;\n            }\n\n            if (_notifyTimerTriggered)\n                return;\n\n            if (_disposed)\n                return;\n\n            if (_notifyTimer is null)\n                return;\n\n            _notifyTimer.Change(NOTIFY_TIMER_INTERVAL, Timeout.Infinite);\n            _notifyTimerTriggered = true;\n        }\n\n        #endregion\n\n        #region record expiry\n\n        protected void InitRecordExpiry()\n        {\n            _recordExpiryTimer = new Timer(RecordExpiryTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n        }\n\n        private uint GetMinRecordExpiryTtl(uint minExpiryTtl)\n        {\n            if (!_recordExpiryTimerRunning)\n                return Math.Min(minExpiryTtl, uint.MaxValue / 1000);\n\n            uint elapsedSeconds = Convert.ToUInt32((DateTime.UtcNow - _recordExpiryTimerStartedOn).TotalSeconds);\n            if (elapsedSeconds >= _recordExpiryTimerTtl)\n                return 0u;\n\n            uint pendingExpiryTtl = _recordExpiryTimerTtl - elapsedSeconds;\n\n            return Math.Min(Math.Min(pendingExpiryTtl, minExpiryTtl), uint.MaxValue / 1000);\n        }\n\n        public void StartRecordExpiryTimer(uint minExpiryTtl)\n        {\n            lock (_recordExpiryTimerLock)\n            {\n                if (_recordExpiryTimer is not null)\n                {\n                    uint minTtl = GetMinRecordExpiryTtl(minExpiryTtl);\n\n                    _recordExpiryTimer.Change(minTtl * 1000, Timeout.Infinite);\n                    _recordExpiryTimerStartedOn = DateTime.UtcNow;\n                    _recordExpiryTimerTtl = minTtl;\n                    _recordExpiryTimerRunning = true;\n                }\n            }\n        }\n\n        private void RecordExpiryTimerCallback(object state)\n        {\n            _recordExpiryTimerRunning = false;\n            uint minExpiryTtl = 0u;\n\n            try\n            {\n                IReadOnlyList<AuthZone> authZones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n                bool recordsDeleted = false;\n\n                foreach (AuthZone authZone in authZones)\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in authZone.Entries)\n                    {\n                        foreach (DnsResourceRecord record in entry.Value)\n                        {\n                            GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();\n                            if (recordInfo.ExpiryTtl > 0u)\n                            {\n                                uint pendingExpiryTtl = recordInfo.GetPendingExpiryTtl();\n                                if (pendingExpiryTtl == 0u)\n                                {\n                                    if (_dnsServer.AuthZoneManager.DeleteRecord(_name, record))\n                                        recordsDeleted = true;\n                                }\n                                else\n                                {\n                                    if (minExpiryTtl == 0u)\n                                        minExpiryTtl = pendingExpiryTtl;\n                                    else\n                                        minExpiryTtl = Math.Min(minExpiryTtl, pendingExpiryTtl);\n                                }\n                            }\n                        }\n                    }\n                }\n\n                if (recordsDeleted)\n                    _dnsServer.AuthZoneManager.SaveZoneFile(_name);\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n            finally\n            {\n                if (minExpiryTtl > 0u)\n                    StartRecordExpiryTimer(minExpiryTtl);\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        internal virtual void UpdateDnssecStatus()\n        {\n            if (!_entries.ContainsKey(DnsResourceRecordType.DNSKEY))\n                _dnssecStatus = AuthZoneDnssecStatus.Unsigned;\n            else if (_entries.ContainsKey(DnsResourceRecordType.NSEC3PARAM))\n                _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3;\n            else\n                _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC;\n        }\n\n        #endregion\n\n        #region versioning\n\n        internal virtual void CommitAndIncrementSerial(IReadOnlyList<DnsResourceRecord> deletedRecords = null, IReadOnlyList<DnsResourceRecord> addedRecords = null)\n        {\n            _lastModified = DateTime.UtcNow;\n\n            if (addedRecords is not null)\n            {\n                uint minExpiryTtl = 0u;\n\n                foreach (DnsResourceRecord addedRecord in addedRecords)\n                {\n                    uint expiryTtl = addedRecord.GetAuthGenericRecordInfo().ExpiryTtl;\n                    if (expiryTtl > 0u)\n                    {\n                        if (minExpiryTtl == 0u)\n                            minExpiryTtl = expiryTtl;\n                        else\n                            minExpiryTtl = Math.Min(minExpiryTtl, expiryTtl);\n                    }\n                }\n\n                if (minExpiryTtl > 0u)\n                    StartRecordExpiryTimer(minExpiryTtl);\n            }\n\n            lock (_zoneHistory)\n            {\n                DnsResourceRecord oldSoaRecord = _entries[DnsResourceRecordType.SOA][0];\n                DnsResourceRecord newSoaRecord;\n                {\n                    DnsSOARecordData oldSoa = oldSoaRecord.RDATA as DnsSOARecordData;\n\n                    if ((addedRecords is not null) && (addedRecords.Count == 1) && (addedRecords[0].Type == DnsResourceRecordType.SOA))\n                    {\n                        DnsResourceRecord addSoaRecord = addedRecords[0];\n                        DnsSOARecordData addSoa = addSoaRecord.RDATA as DnsSOARecordData;\n\n                        uint serial = GetNewSerial(oldSoa.Serial, addSoa.Serial, addSoaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme);\n\n                        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 };\n                        addedRecords = null;\n                    }\n                    else\n                    {\n                        uint serial = GetNewSerial(oldSoa.Serial, 0, oldSoaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme);\n\n                        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 };\n                    }\n                }\n\n                DnsResourceRecord[] newSoaRecords = [newSoaRecord];\n\n                //update SOA\n                _entries[DnsResourceRecordType.SOA] = newSoaRecords;\n\n                IReadOnlyList<DnsResourceRecord> newRRSigRecords = null;\n                IReadOnlyList<DnsResourceRecord> deletedRRSigRecords = null;\n\n                if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                {\n                    //sign SOA and update RRSig\n                    newRRSigRecords = SignRRSet(newSoaRecords);\n                    AddOrUpdateRRSigRecords(newRRSigRecords, out deletedRRSigRecords);\n                }\n\n                //remove RR info from old SOA to allow creating new history RR info for setting DeletedOn\n                oldSoaRecord.Tag = null;\n\n                //start commit\n                oldSoaRecord.GetAuthHistoryRecordInfo().DeletedOn = DateTime.UtcNow;\n\n                //write removed\n                _zoneHistory.Add(oldSoaRecord);\n\n                if (deletedRecords is not null)\n                {\n                    foreach (DnsResourceRecord deletedRecord in deletedRecords)\n                    {\n                        if (deletedRecord.GetAuthGenericRecordInfo().Disabled)\n                            continue;\n\n                        _zoneHistory.Add(deletedRecord);\n\n                        if (deletedRecord.Type == DnsResourceRecordType.NS)\n                        {\n                            IReadOnlyList<DnsResourceRecord> glueRecords = deletedRecord.GetAuthNSRecordInfo().GlueRecords;\n                            if (glueRecords is not null)\n                                _zoneHistory.AddRange(glueRecords);\n                        }\n                    }\n                }\n\n                if (deletedRRSigRecords is not null)\n                    _zoneHistory.AddRange(deletedRRSigRecords);\n\n                //write added\n                _zoneHistory.Add(newSoaRecord);\n\n                if (addedRecords is not null)\n                {\n                    foreach (DnsResourceRecord addedRecord in addedRecords)\n                    {\n                        if (addedRecord.GetAuthGenericRecordInfo().Disabled)\n                            continue;\n\n                        _zoneHistory.Add(addedRecord);\n\n                        if (addedRecord.Type == DnsResourceRecordType.NS)\n                        {\n                            IReadOnlyList<DnsResourceRecord> glueRecords = addedRecord.GetAuthNSRecordInfo().GlueRecords;\n                            if (glueRecords is not null)\n                                _zoneHistory.AddRange(glueRecords);\n                        }\n                    }\n                }\n\n                if (newRRSigRecords is not null)\n                    _zoneHistory.AddRange(newRRSigRecords);\n\n                //end commit\n\n                CleanupHistory();\n            }\n        }\n\n        protected static uint GetNewSerial(uint oldSerial, uint updateSerial, bool useSoaSerialDateScheme)\n        {\n            if (useSoaSerialDateScheme)\n            {\n                string strOldSerial = oldSerial.ToString();\n                string strOldSerialDate = null;\n                byte counter = 0;\n\n                if (strOldSerial.Length == 10)\n                {\n                    //parse old serial\n                    strOldSerialDate = strOldSerial.Substring(0, 8);\n                    counter = byte.Parse(strOldSerial.Substring(8));\n                }\n\n                string strSerialDate = DateTime.UtcNow.ToString(\"yyyyMMdd\");\n\n                if (strOldSerialDate is null)\n                {\n                    //transitioning to date scheme\n                    return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0'));\n                }\n                else if (strSerialDate.Equals(strOldSerialDate))\n                {\n                    //same date\n                    if (counter < 99)\n                    {\n                        counter++;\n                        return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0'));\n                    }\n                    else\n                    {\n                        //more than 100 increments\n                        return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0')) + 1;\n                    }\n                }\n                else if (uint.Parse(strSerialDate) > uint.Parse(strOldSerialDate))\n                {\n                    //later date\n                    return uint.Parse(strSerialDate + \"00\");\n                }\n            }\n\n            //default\n            uint serial = oldSerial;\n\n            if (updateSerial > serial)\n                serial = updateSerial;\n            else if (serial < uint.MaxValue)\n                serial++;\n            else\n                serial = 1;\n\n            return serial;\n        }\n\n        internal void SetSoaSerial(uint newSerial)\n        {\n            lock (_zoneHistory)\n            {\n                DnsResourceRecord oldSoaRecord = _entries[DnsResourceRecordType.SOA][0];\n                DnsSOARecordData oldSoa = oldSoaRecord.RDATA as DnsSOARecordData;\n\n                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 };\n                DnsResourceRecord[] newSoaRecords = [newSoaRecord];\n\n                //update SOA\n                _entries[DnsResourceRecordType.SOA] = newSoaRecords;\n\n                //clear history\n                _zoneHistory.Clear();\n            }\n        }\n\n        public IReadOnlyList<DnsResourceRecord> GetZoneHistory()\n        {\n            lock (_zoneHistory)\n            {\n                return _zoneHistory.ToArray();\n            }\n        }\n\n        protected void CleanupHistory()\n        {\n            DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;\n            DateTime expiry = DateTime.UtcNow.AddSeconds(-soa.Expire);\n            int index = 0;\n\n            while (index < _zoneHistory.Count)\n            {\n                //check difference sequence\n                if (_zoneHistory[index].GetAuthHistoryRecordInfo().DeletedOn > expiry)\n                    break; //found record to keep\n\n                //skip to next difference sequence\n                index++;\n                int soaCount = 1;\n\n                while (index < _zoneHistory.Count)\n                {\n                    if (_zoneHistory[index].Type == DnsResourceRecordType.SOA)\n                    {\n                        soaCount++;\n\n                        if (soaCount == 3)\n                            break;\n                    }\n\n                    index++;\n                }\n            }\n\n            if (index == _zoneHistory.Count)\n            {\n                //delete entire history\n                _zoneHistory.Clear();\n                return;\n            }\n\n            //remove expired records\n            _zoneHistory.RemoveRange(0, index);\n        }\n\n        protected void CommitZoneHistory(IReadOnlyList<DnsResourceRecord> historyRecords)\n        {\n            lock (_zoneHistory)\n            {\n                historyRecords[0].GetAuthHistoryRecordInfo().DeletedOn = DateTime.UtcNow;\n\n                //write history\n                _zoneHistory.AddRange(historyRecords);\n\n                CleanupHistory();\n            }\n        }\n\n        protected void ClearZoneHistory()\n        {\n            lock (_zoneHistory)\n            {\n                _zoneHistory.Clear();\n            }\n        }\n\n        #endregion\n\n        #region catalog zone\n\n        private IReadOnlyCollection<NetworkAccessControl> GetQueryAccessACL()\n        {\n            switch (_queryAccess)\n            {\n                case AuthZoneQueryAccess.Allow:\n                    return [\n                                new NetworkAccessControl(IPAddress.Any, 0),\n                                new NetworkAccessControl(IPAddress.IPv6Any, 0)\n                           ];\n\n                case AuthZoneQueryAccess.AllowOnlyPrivateNetworks:\n                    return [\n                                new NetworkAccessControl(IPAddress.Parse(\"127.0.0.0\"), 8),\n                                new NetworkAccessControl(IPAddress.Parse(\"10.0.0.0\"), 8),\n                                new NetworkAccessControl(IPAddress.Parse(\"100.64.0.0\"), 10),\n                                new NetworkAccessControl(IPAddress.Parse(\"169.254.0.0\"), 16),\n                                new NetworkAccessControl(IPAddress.Parse(\"172.16.0.0\"), 12),\n                                new NetworkAccessControl(IPAddress.Parse(\"192.168.0.0\"), 16),\n                                new NetworkAccessControl(IPAddress.Parse(\"2000::\"), 3, true),\n                                new NetworkAccessControl(IPAddress.IPv6Any, 0)\n                           ];\n\n                case AuthZoneQueryAccess.AllowOnlyZoneNameServers:\n                    return [\n                                new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32)\n                           ];\n\n                case AuthZoneQueryAccess.UseSpecifiedNetworkACL:\n                    return _queryAccessNetworkACL;\n\n                case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                    if (_queryAccessNetworkACL is null)\n                    {\n                        return [\n                                    new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32)\n                                ];\n                    }\n\n                    return [\n                                new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32),\n                                .._queryAccessNetworkACL\n                           ];\n\n                case AuthZoneQueryAccess.Deny:\n                default:\n                    return [\n                                new NetworkAccessControl(IPAddress.Parse(\"127.0.0.0\"), 8),\n                                new NetworkAccessControl(IPAddress.Parse(\"::1\"), 128)\n                           ];\n            }\n        }\n\n        private IReadOnlyCollection<NetworkAccessControl> GetZoneTranferACL()\n        {\n            switch (_zoneTransfer)\n            {\n                case AuthZoneTransfer.Allow:\n                    return [\n                                new NetworkAccessControl(IPAddress.Any, 0),\n                                new NetworkAccessControl(IPAddress.IPv6Any, 0)\n                           ];\n\n                case AuthZoneTransfer.AllowOnlyZoneNameServers:\n                    return [\n                                new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32)\n                           ];\n\n                case AuthZoneTransfer.UseSpecifiedNetworkACL:\n                    return _zoneTransferNetworkACL;\n\n                case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                    if (_zoneTransferNetworkACL is null)\n                    {\n                        return [\n                                    new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32)\n                                ];\n                    }\n\n                    return [\n                                new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32),\n                                .._zoneTransferNetworkACL\n                           ];\n\n                case AuthZoneTransfer.Deny:\n                default:\n                    return [\n                                new NetworkAccessControl(IPAddress.Any, 0, true),\n                                new NetworkAccessControl(IPAddress.IPv6Any, 0, true)\n                           ];\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public uint GetZoneSoaSerial()\n        {\n            return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Serial;\n        }\n\n        public uint GetZoneSoaRetry()\n        {\n            return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Retry;\n        }\n\n        public uint GetZoneSoaExpire()\n        {\n            return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Expire;\n        }\n\n        public uint GetZoneSoaMinimum()\n        {\n            return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Minimum;\n        }\n\n        public abstract string GetZoneTypeName();\n\n        public override string ToString()\n        {\n            return _name.Length == 0 ? \"<root>\" : _name;\n        }\n\n        #endregion\n\n        #region name server address resolution\n\n        public async Task<IReadOnlyList<NameServerAddress>> GetResolvedPrimaryNameServerAddressesAsync()\n        {\n            IReadOnlyList<NameServerAddress> primaryNameServers;\n\n            if (this is SecondaryZone secondary)\n                primaryNameServers = secondary.PrimaryNameServerAddresses;\n            else if (this is StubZone stub)\n                primaryNameServers = stub.PrimaryNameServerAddresses;\n            else\n                primaryNameServers = null;\n\n            if (primaryNameServers is not null)\n                return await GetResolvedNameServerAddressesAsync(primaryNameServers);\n\n            DnsResourceRecord soaRecord = _entries[DnsResourceRecordType.SOA][0];\n            string primaryNameServer = (soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer;\n            IReadOnlyList<DnsResourceRecord> nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords\n\n            List<NameServerAddress> nameServers = new List<NameServerAddress>(nsRecords.Count * 2);\n\n            foreach (DnsResourceRecord nsRecord in nsRecords)\n            {\n                if (nsRecord.GetAuthGenericRecordInfo().Disabled)\n                    continue;\n\n                if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase))\n                {\n                    //found primary NS\n                    await ResolveNameServerAddressesAsync(nsRecord, nameServers);\n                    break;\n                }\n            }\n\n            if (nameServers.Count < 1)\n                await ResolveNameServerAddressesAsync(primaryNameServer, 53, DnsTransportProtocol.Udp, nameServers);\n\n            return nameServers;\n        }\n\n        public async Task<IReadOnlyList<NameServerAddress>> GetResolvedSecondaryNameServerAddressesAsync()\n        {\n            string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer;\n            IReadOnlyList<DnsResourceRecord> nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords\n\n            List<NameServerAddress> nameServers = new List<NameServerAddress>(nsRecords.Count * 2);\n\n            foreach (DnsResourceRecord nsRecord in nsRecords)\n            {\n                if (nsRecord.GetAuthGenericRecordInfo().Disabled)\n                    continue;\n\n                if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase))\n                    continue; //skip primary name server\n\n                await ResolveNameServerAddressesAsync(nsRecord, nameServers);\n            }\n\n            return nameServers;\n        }\n\n        public async Task<IReadOnlyList<NameServerAddress>> GetAllResolvedNameServerAddressesAsync()\n        {\n            IReadOnlyList<DnsResourceRecord> nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords\n\n            List<NameServerAddress> nameServers = new List<NameServerAddress>(nsRecords.Count * 2);\n\n            foreach (DnsResourceRecord nsRecord in nsRecords)\n            {\n                if (nsRecord.GetAuthGenericRecordInfo().Disabled)\n                    continue;\n\n                await ResolveNameServerAddressesAsync(nsRecord, nameServers);\n            }\n\n            return nameServers;\n        }\n\n        public async Task<IReadOnlyList<NameServerAddress>> GetResolvedNameServerAddressesAsync(IReadOnlyList<NameServerAddress> nameServers)\n        {\n            List<NameServerAddress> resolvedNameServers = new List<NameServerAddress>(nameServers.Count * 2);\n            List<Task> resolverTasks = new List<Task>(nameServers.Count);\n\n            foreach (NameServerAddress nameServer in nameServers)\n            {\n                if (nameServer.IsIPEndPointStale)\n                    resolverTasks.Add(ResolveNameServerAddressesAsync(nameServer.Host, nameServer.Port, nameServer.Protocol, resolvedNameServers));\n                else\n                    resolvedNameServers.Add(nameServer);\n            }\n\n            await Task.WhenAll(resolverTasks);\n\n            return resolvedNameServers;\n        }\n\n        private async Task ResolveNameServerAddressesAsync(string nsDomain, int port, DnsTransportProtocol protocol, List<NameServerAddress> outNameServers, CancellationToken cancellationToken = default)\n        {\n            try\n            {\n                DnsDatagram response = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.A, DnsClass.IN), cancellationToken: cancellationToken);\n                if (response.Answer.Count > 0)\n                {\n                    IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseA(response);\n                    foreach (IPAddress address in addresses)\n                        outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol));\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.ResolverLogManager?.Write(ex);\n            }\n\n            if (_dnsServer.PreferIPv6)\n            {\n                try\n                {\n                    DnsDatagram response = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.AAAA, DnsClass.IN), cancellationToken: cancellationToken);\n                    if (response.Answer.Count > 0)\n                    {\n                        IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseAAAA(response);\n                        foreach (IPAddress address in addresses)\n                            outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol));\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.ResolverLogManager?.Write(ex);\n                }\n            }\n        }\n\n        private Task ResolveNameServerAddressesAsync(DnsResourceRecord nsRecord, List<NameServerAddress> outNameServers)\n        {\n            string nsDomain = (nsRecord.RDATA as DnsNSRecordData).NameServer;\n\n            IReadOnlyList<DnsResourceRecord> glueRecords = nsRecord.GetAuthNSRecordInfo().GlueRecords;\n            if (glueRecords is not null)\n            {\n                foreach (DnsResourceRecord glueRecord in glueRecords)\n                {\n                    switch (glueRecord.Type)\n                    {\n                        case DnsResourceRecordType.A:\n                            outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsARecordData).Address));\n                            break;\n\n                        case DnsResourceRecordType.AAAA:\n                            if (_dnsServer.PreferIPv6)\n                                outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsAAAARecordData).Address));\n\n                            break;\n                    }\n                }\n\n                return Task.CompletedTask;\n            }\n            else\n            {\n                return ResolveNameServerAddressesAsync(nsDomain, 53, DnsTransportProtocol.Udp, outNameServers);\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool Disabled\n        {\n            get { return base.Disabled; }\n            set\n            {\n                if (base.Disabled == value)\n                    return;\n\n                base.Disabled = value; //set value early to be able to use it for setting catalog properties\n\n                CatalogZone catalogZone = CatalogZone;\n                if (catalogZone is not null)\n                {\n                    if (value)\n                        _dnsServer.AuthZoneManager.RemoveCatalogMemberZone(new AuthZoneInfo(this), true); //remove catalog zone membership without removing it from zone's options\n                    else\n                        _dnsServer.AuthZoneManager.AddCatalogMemberZone(_catalogZoneName, new AuthZoneInfo(this), true); //add catalog zone membership\n                }\n            }\n        }\n\n        public DateTime LastModified\n        { get { return _lastModified; } }\n\n        public virtual string CatalogZoneName\n        {\n            get { return _catalogZoneName; }\n            set\n            {\n                if (string.IsNullOrEmpty(value))\n                    _catalogZoneName = null;\n                else\n                    _catalogZoneName = value;\n\n                //reset\n                _catalogZone = null;\n                _secondaryCatalogZone = null;\n            }\n        }\n\n        public virtual bool OverrideCatalogQueryAccess\n        {\n            get { return _overrideCatalogQueryAccess; }\n            set { _overrideCatalogQueryAccess = value; }\n        }\n\n        public virtual bool OverrideCatalogZoneTransfer\n        {\n            get { return _overrideCatalogZoneTransfer; }\n            set { _overrideCatalogZoneTransfer = value; }\n        }\n\n        public virtual bool OverrideCatalogNotify\n        {\n            get { return _overrideCatalogNotify; }\n            set { _overrideCatalogNotify = value; }\n        }\n\n        public virtual AuthZoneQueryAccess QueryAccess\n        {\n            get { return _queryAccess; }\n            set\n            {\n                _queryAccess = value;\n\n                //update catalog zone property\n                if (this is CatalogZone thisCatalogZone)\n                {\n                    //update global custom property\n                    thisCatalogZone.SetAllowQueryProperty(GetQueryAccessACL());\n                }\n                else if (!Disabled && ((this is PrimaryZone) || (this is SecondaryZone && this is not SecondaryForwarderZone) || (this is StubZone) || (this is ForwarderZone)))\n                {\n                    CatalogZone catalogZone = CatalogZone;\n                    if (catalogZone is not null)\n                    {\n                        if (_overrideCatalogQueryAccess)\n                            catalogZone.SetAllowQueryProperty(GetQueryAccessACL(), _name); //update member zone custom property\n                        else\n                            catalogZone.SetAllowQueryProperty(null, _name); //remove member zone custom property\n                    }\n                }\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> QueryAccessNetworkACL\n        {\n            get { return _queryAccessNetworkACL; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _queryAccessNetworkACL = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(QueryAccessNetworkACL), \"Network ACL cannot have more than 255 entries.\");\n                else\n                    _queryAccessNetworkACL = value;\n            }\n        }\n\n        public virtual AuthZoneTransfer ZoneTransfer\n        {\n            get { return _zoneTransfer; }\n            set\n            {\n                _zoneTransfer = value;\n\n                //update catalog zone property\n                if (this is CatalogZone thisCatalogZone)\n                {\n                    //update global custom property\n                    thisCatalogZone.SetAllowTransferProperty(GetZoneTranferACL());\n                }\n                else if (!Disabled && ((this is PrimaryZone) || (this is SecondaryZone && this is not SecondaryForwarderZone)))\n                {\n                    CatalogZone catalogZone = CatalogZone;\n                    if (catalogZone is not null)\n                    {\n                        if (_overrideCatalogZoneTransfer)\n                            catalogZone.SetAllowTransferProperty(GetZoneTranferACL(), _name); //update member zone custom property\n                        else\n                            catalogZone.SetAllowTransferProperty(null, _name); //remove member zone custom property\n                    }\n                }\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> ZoneTransferNetworkACL\n        {\n            get { return _zoneTransferNetworkACL; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _zoneTransferNetworkACL = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(ZoneTransferNetworkACL), \"Network ACL cannot have more than 255 entries.\");\n                else\n                    _zoneTransferNetworkACL = value;\n            }\n        }\n\n        public IReadOnlySet<string> ZoneTransferTsigKeyNames\n        {\n            get { return _zoneTransferTsigKeyNames; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _zoneTransferTsigKeyNames = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(ZoneTransferTsigKeyNames), \"Zone transfer TSIG key names cannot have more than 255 entries.\");\n                else\n                    _zoneTransferTsigKeyNames = value;\n\n                //update catalog zone property\n                if (this is CatalogZone thisCatalogZone)\n                {\n                    //update global custom property\n                    thisCatalogZone.SetZoneTransferTsigKeyNamesProperty(_zoneTransferTsigKeyNames);\n                }\n                else if (!Disabled && ((this is PrimaryZone) || (this is SecondaryZone && this is not SecondaryForwarderZone)))\n                {\n                    CatalogZone catalogZone = CatalogZone;\n                    if (catalogZone is not null)\n                    {\n                        if (_overrideCatalogZoneTransfer)\n                            catalogZone.SetZoneTransferTsigKeyNamesProperty(_zoneTransferTsigKeyNames, _name); //update member zone custom property\n                        else\n                            catalogZone.SetZoneTransferTsigKeyNamesProperty(null, _name); //remove member zone custom property\n                    }\n                }\n            }\n        }\n\n        public virtual AuthZoneNotify Notify\n        {\n            get { return _notify; }\n            set\n            {\n                _notify = value;\n\n                lock (_notifyFailed)\n                {\n                    _notifyFailed.Clear();\n                }\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> NotifyNameServers\n        {\n            get { return _notifyNameServers; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _notifyNameServers = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(NotifyNameServers), \"Name server addresses cannot have more than 255 entries.\");\n                else\n                    _notifyNameServers = value;\n\n                lock (_notifyFailed)\n                {\n                    _notifyFailed.Clear();\n                }\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> NotifySecondaryCatalogNameServers\n        {\n            get { return _notifySecondaryCatalogNameServers; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _notifySecondaryCatalogNameServers = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(NotifySecondaryCatalogNameServers), \"Secondary Catalog name server addresses cannot have more than 255 entries.\");\n                else\n                    _notifySecondaryCatalogNameServers = value;\n\n                lock (_notifyFailed)\n                {\n                    _notifyFailed.Clear();\n                }\n            }\n        }\n\n        public virtual AuthZoneUpdate Update\n        {\n            get { return _update; }\n            set { _update = value; }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> UpdateNetworkACL\n        {\n            get { return _updateNetworkACL; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _updateNetworkACL = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(UpdateNetworkACL), \"Network ACL cannot have more than 255 entries.\");\n                else\n                    _updateNetworkACL = value;\n            }\n        }\n\n        public IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> UpdateSecurityPolicies\n        {\n            get { return _updateSecurityPolicies; }\n            set { _updateSecurityPolicies = value; }\n        }\n\n        public AuthZoneDnssecStatus DnssecStatus\n        { get { return _dnssecStatus; } }\n\n        public string[] NotifyFailed\n        {\n            get\n            {\n                if (_notifyFailed is null)\n                    return Array.Empty<string>();\n\n                lock (_notifyFailed)\n                {\n                    if (_notifyFailed.Count > 0)\n                        return _notifyFailed.ToArray();\n\n                    return Array.Empty<string>();\n                }\n            }\n        }\n\n        public bool SyncFailed\n        { get { return _syncFailed; } }\n\n        public CatalogZone CatalogZone\n        {\n            get\n            {\n                if (_catalogZoneName is null)\n                    return null;\n\n                if (_secondaryCatalogZone is not null)\n                    return null;\n\n                if (_catalogZone is null)\n                {\n                    if ((this is PrimaryZone) || (this is ForwarderZone))\n                    {\n                        ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName);\n                        if (apexZone is CatalogZone catalogZone)\n                            _catalogZone = catalogZone;\n                    }\n                    else if ((this is StubZone) || (this is SecondaryZone && this is not SecondaryForwarderZone))\n                    {\n                        ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName);\n                        if (apexZone is CatalogZone catalogZone)\n                            _catalogZone = catalogZone;\n                        else if (apexZone is SecondaryCatalogZone secondaryCatalogZone)\n                            _secondaryCatalogZone = secondaryCatalogZone;\n                    }\n                }\n\n                return _catalogZone;\n            }\n        }\n\n        public SecondaryCatalogZone SecondaryCatalogZone\n        {\n            get\n            {\n                if (_catalogZoneName is null)\n                    return null;\n\n                if (_catalogZone is not null)\n                    return null;\n\n                if (_secondaryCatalogZone is null)\n                {\n                    if (this is SecondaryZone)\n                    {\n                        ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName);\n                        if (apexZone is SecondaryCatalogZone secondaryCatalogZone)\n                            _secondaryCatalogZone = secondaryCatalogZone;\n                    }\n                    else if (this is StubZone)\n                    {\n                        ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName);\n                        if (apexZone is SecondaryCatalogZone secondaryCatalogZone)\n                            _secondaryCatalogZone = secondaryCatalogZone;\n                        else if (apexZone is CatalogZone catalogZone)\n                            _catalogZone = catalogZone;\n                    }\n                }\n\n                return _secondaryCatalogZone;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/AuthZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    abstract class AuthZone : Zone\n    {\n        #region variables\n\n        bool _disabled;\n\n        #endregion\n\n        #region constructor\n\n        protected AuthZone(AuthZoneInfo zoneInfo)\n            : base(zoneInfo.Name)\n        {\n            _disabled = zoneInfo.Disabled;\n        }\n\n        protected AuthZone(string name)\n            : base(name)\n        { }\n\n        #endregion\n\n        #region private\n\n        private IReadOnlyList<DnsResourceRecord> FilterDisabledRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            if (_disabled)\n                return Array.Empty<DnsResourceRecord>();\n\n            if (records.Count == 1)\n            {\n                GenericRecordInfo authRecordInfo = records[0].GetAuthGenericRecordInfo();\n\n                if (authRecordInfo.Disabled)\n                    return Array.Empty<DnsResourceRecord>(); //record disabled\n\n                //update last used on\n                authRecordInfo.LastUsedOn = DateTime.UtcNow;\n\n                return records;\n            }\n\n            List<DnsResourceRecord> newRecords = new List<DnsResourceRecord>(records.Count);\n            DateTime utcNow = DateTime.UtcNow;\n\n            foreach (DnsResourceRecord record in records)\n            {\n                GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo();\n\n                if (authRecordInfo.Disabled)\n                    continue; //record disabled\n\n                //update last used on\n                authRecordInfo.LastUsedOn = utcNow;\n\n                newRecords.Add(record);\n            }\n\n            if (newRecords.Count > 1)\n            {\n                switch (type)\n                {\n                    case DnsResourceRecordType.A:\n                    case DnsResourceRecordType.AAAA:\n                    case DnsResourceRecordType.NS:\n                        newRecords.Shuffle(); //shuffle records to allow load balancing\n                        break;\n                }\n            }\n\n            return newRecords;\n        }\n\n        private IReadOnlyList<DnsResourceRecord> AppendRRSigTo(IReadOnlyList<DnsResourceRecord> records)\n        {\n            IReadOnlyList<DnsResourceRecord> rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG);\n            if (rrsigRecords.Count == 0)\n                return records;\n\n            DnsResourceRecordType type = records[0].Type;\n            List<DnsResourceRecord> newRecords = new List<DnsResourceRecord>(records.Count + 2);\n            newRecords.AddRange(records);\n\n            DateTime utcNow = DateTime.UtcNow;\n\n            foreach (DnsResourceRecord rrsigRecord in rrsigRecords)\n            {\n                if ((rrsigRecord.RDATA as DnsRRSIGRecordData).TypeCovered == type)\n                {\n                    rrsigRecord.GetAuthGenericRecordInfo().LastUsedOn = utcNow;\n                    newRecords.Add(rrsigRecord);\n                }\n            }\n\n            return newRecords;\n        }\n\n        #endregion\n\n        #region versioning\n\n        internal bool TrySetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records, out IReadOnlyList<DnsResourceRecord> deletedRecords)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.CNAME:\n                    if ((!_entries.IsEmpty) && !_entries.ContainsKey(DnsResourceRecordType.CNAME))\n                        throw new InvalidOperationException(\"Cannot add record: a CNAME record cannot exists with other record types for the same name.\");\n\n                    break;\n\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.RRSIG:\n                    break; //ignore\n\n                default:\n                    if (_entries.ContainsKey(DnsResourceRecordType.CNAME))\n                        throw new InvalidOperationException(\"Cannot add record: a CNAME record cannot exists with other record types for the same name.\");\n\n                    break;\n            }\n\n            if (_entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n            {\n                deletedRecords = existingRecords;\n                return _entries.TryUpdate(type, records, existingRecords);\n            }\n            else\n            {\n                deletedRecords = Array.Empty<DnsResourceRecord>();\n                return _entries.TryAdd(type, records);\n            }\n        }\n\n        internal bool TryDeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata, out DnsResourceRecord deletedRecord)\n        {\n            if (_entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n            {\n                if (existingRecords.Count == 1)\n                {\n                    if (rdata.Equals(existingRecords[0].RDATA))\n                    {\n                        if (_entries.TryRemove(type, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                        {\n                            deletedRecord = removedRecords[0];\n                            return true;\n                        }\n                    }\n                }\n                else\n                {\n                    deletedRecord = null;\n                    List<DnsResourceRecord> updatedRecords = new List<DnsResourceRecord>(existingRecords.Count);\n\n                    foreach (DnsResourceRecord existingRecord in existingRecords)\n                    {\n                        if ((deletedRecord is null) && rdata.Equals(existingRecord.RDATA))\n                            deletedRecord = existingRecord;\n                        else\n                            updatedRecords.Add(existingRecord);\n                    }\n\n                    if (deletedRecord is null)\n                        return false; //not found\n\n                    return _entries.TryUpdate(type, updatedRecords, existingRecords);\n                }\n            }\n\n            deletedRecord = null;\n            return false;\n        }\n\n        internal bool TryDeleteRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records, out IReadOnlyList<DnsResourceRecord> deletedRecords)\n        {\n            if (_entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n            {\n                if (existingRecords.Count == 1)\n                {\n                    DnsResourceRecord existingRecord = existingRecords[0];\n\n                    foreach (DnsResourceRecord record in records)\n                    {\n                        if (record.RDATA.Equals(existingRecord.RDATA))\n                        {\n                            if (_entries.TryRemove(type, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                            {\n                                deletedRecords = removedRecords;\n                                return true;\n                            }\n                        }\n                    }\n                }\n                else\n                {\n                    List<DnsResourceRecord> deleted = new List<DnsResourceRecord>(records.Count);\n                    List<DnsResourceRecord> updatedRecords = new List<DnsResourceRecord>(existingRecords.Count);\n\n                    foreach (DnsResourceRecord existingRecord in existingRecords)\n                    {\n                        bool found = false;\n\n                        foreach (DnsResourceRecord record in records)\n                        {\n                            if (record.RDATA.Equals(existingRecord.RDATA))\n                            {\n                                found = true;\n                                break;\n                            }\n                        }\n\n                        if (found)\n                            deleted.Add(existingRecord);\n                        else\n                            updatedRecords.Add(existingRecord);\n                    }\n\n                    if (deleted.Count > 0)\n                    {\n                        deletedRecords = deleted;\n\n                        if (updatedRecords.Count > 0)\n                            return _entries.TryUpdate(type, updatedRecords, existingRecords);\n\n                        return _entries.TryRemove(type, out _);\n                    }\n                }\n            }\n\n            deletedRecords = null;\n            return false;\n        }\n\n        internal void AddOrUpdateRRSigRecords(IReadOnlyList<DnsResourceRecord> newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords)\n        {\n            IReadOnlyList<DnsResourceRecord> deleted = null;\n\n            _entries.AddOrUpdate(DnsResourceRecordType.RRSIG, delegate (DnsResourceRecordType key)\n            {\n                deleted = Array.Empty<DnsResourceRecord>();\n                return newRRSigRecords;\n            },\n            delegate (DnsResourceRecordType key, IReadOnlyList<DnsResourceRecord> existingRecords)\n            {\n                List<DnsResourceRecord> updatedRecords = new List<DnsResourceRecord>(existingRecords.Count + newRRSigRecords.Count);\n                List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n                foreach (DnsResourceRecord existingRecord in existingRecords)\n                {\n                    bool found = false;\n                    DnsRRSIGRecordData existingRRSig = existingRecord.RDATA as DnsRRSIGRecordData;\n\n                    foreach (DnsResourceRecord newRRSigRecord in newRRSigRecords)\n                    {\n                        DnsRRSIGRecordData newRRSig = newRRSigRecord.RDATA as DnsRRSIGRecordData;\n\n                        if ((newRRSig.TypeCovered == existingRRSig.TypeCovered) && (newRRSig.KeyTag == existingRRSig.KeyTag))\n                        {\n                            deletedRecords.Add(existingRecord);\n                            found = true;\n                            break;\n                        }\n                    }\n\n                    if (!found)\n                        updatedRecords.Add(existingRecord);\n                }\n\n                updatedRecords.AddRange(newRRSigRecords);\n\n                deleted = deletedRecords;\n                return updatedRecords;\n            });\n\n            deletedRRSigRecords = deleted;\n        }\n\n        internal void AddRecord(DnsResourceRecord record, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords)\n        {\n            switch (record.Type)\n            {\n                case DnsResourceRecordType.CNAME:\n                case DnsResourceRecordType.DNAME:\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot add record: use SetRecords() for \" + record.Type.ToString() + \" record.\");\n\n                default:\n                    if (_entries.ContainsKey(DnsResourceRecordType.CNAME))\n                        throw new InvalidOperationException(\"Cannot add record: a CNAME record cannot exists with other record types for the same name.\");\n\n                    break;\n            }\n\n            List<DnsResourceRecord> added = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deleted = new List<DnsResourceRecord>();\n\n            addedRecords = added;\n            deletedRecords = deleted;\n\n            _entries.AddOrUpdate(record.Type, delegate (DnsResourceRecordType key)\n            {\n                added.Add(record);\n                return new DnsResourceRecord[] { record };\n            },\n            delegate (DnsResourceRecordType key, IReadOnlyList<DnsResourceRecord> existingRecords)\n            {\n                foreach (DnsResourceRecord existingRecord in existingRecords)\n                {\n                    if (record.RDATA.Equals(existingRecord.RDATA))\n                        return existingRecords;\n                }\n\n                List<DnsResourceRecord> updatedRecords = new List<DnsResourceRecord>(existingRecords.Count + 1);\n\n                foreach (DnsResourceRecord existingRecord in existingRecords)\n                {\n                    if (existingRecord.OriginalTtlValue == record.OriginalTtlValue)\n                    {\n                        updatedRecords.Add(existingRecord);\n                    }\n                    else\n                    {\n                        DnsResourceRecord updatedExistingRecord = new DnsResourceRecord(existingRecord.Name, existingRecord.Type, existingRecord.Class, record.OriginalTtlValue, existingRecord.RDATA);\n                        updatedRecords.Add(updatedExistingRecord);\n\n                        added.Add(updatedExistingRecord);\n                        deleted.Add(existingRecord);\n                    }\n                }\n\n                updatedRecords.Add(record);\n\n                added.Add(record);\n                return updatedRecords;\n            });\n        }\n\n        #endregion\n\n        #region catalog zones\n\n        protected IEnumerable<KeyValuePair<string, string>> EnumerateCatalogMemberZones(DnsServer dnsServer)\n        {\n            List<string> subDomains = new List<string>();\n            dnsServer.AuthZoneManager.ListSubDomains(\"zones.\" + _name, subDomains);\n\n            foreach (string subDomain in subDomains)\n            {\n                IReadOnlyList<DnsResourceRecord> ptrRecords = dnsServer.AuthZoneManager.GetRecords(_name, subDomain + \".zones.\" + _name, DnsResourceRecordType.PTR);\n                if (ptrRecords.Count > 0)\n                    yield return new KeyValuePair<string, string>((ptrRecords[0].RDATA as DnsPTRRecordData).Domain, ptrRecords[0].Name);\n            }\n        }\n\n        #endregion\n\n        #region DNSSEC\n\n        internal IReadOnlyList<DnsResourceRecord> SignAllRRSets()\n        {\n            List<DnsResourceRecord> rrsigRecords = new List<DnsResourceRecord>(_entries.Count);\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                if (entry.Key == DnsResourceRecordType.RRSIG)\n                    continue;\n\n                rrsigRecords.AddRange(SignRRSet(entry.Value));\n            }\n\n            return rrsigRecords;\n        }\n\n        internal IReadOnlyList<DnsResourceRecord> RemoveAllDnssecRecords()\n        {\n            List<DnsResourceRecord> allRemovedRecords = new List<DnsResourceRecord>();\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                switch (entry.Key)\n                {\n                    case DnsResourceRecordType.DNSKEY:\n                    case DnsResourceRecordType.RRSIG:\n                    case DnsResourceRecordType.NSEC:\n                    case DnsResourceRecordType.NSEC3PARAM:\n                    case DnsResourceRecordType.NSEC3:\n                        if (_entries.TryRemove(entry.Key, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                            allRemovedRecords.AddRange(removedRecords);\n\n                        break;\n                }\n            }\n\n            return allRemovedRecords;\n        }\n\n        internal IReadOnlyList<DnsResourceRecord> RemoveNSecRecordsWithRRSig()\n        {\n            List<DnsResourceRecord> allRemovedRecords = new List<DnsResourceRecord>(2);\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                switch (entry.Key)\n                {\n                    case DnsResourceRecordType.NSEC:\n                        if (_entries.TryRemove(entry.Key, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                            allRemovedRecords.AddRange(removedRecords);\n\n                        break;\n\n                    case DnsResourceRecordType.RRSIG:\n                        List<DnsResourceRecord> recordsToRemove = new List<DnsResourceRecord>(1);\n\n                        foreach (DnsResourceRecord rrsigRecord in entry.Value)\n                        {\n                            DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;\n                            if (rrsig.TypeCovered == DnsResourceRecordType.NSEC)\n                                recordsToRemove.Add(rrsigRecord);\n                        }\n\n                        if (recordsToRemove.Count > 0)\n                        {\n                            if (TryDeleteRecords(DnsResourceRecordType.RRSIG, recordsToRemove, out IReadOnlyList<DnsResourceRecord> deletedRecords))\n                                allRemovedRecords.AddRange(deletedRecords);\n                        }\n\n                        break;\n                }\n            }\n\n            return allRemovedRecords;\n        }\n\n        internal IReadOnlyList<DnsResourceRecord> RemoveNSec3RecordsWithRRSig()\n        {\n            List<DnsResourceRecord> allRemovedRecords = new List<DnsResourceRecord>(2);\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                switch (entry.Key)\n                {\n                    case DnsResourceRecordType.NSEC3:\n                    case DnsResourceRecordType.NSEC3PARAM:\n                        if (_entries.TryRemove(entry.Key, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                            allRemovedRecords.AddRange(removedRecords);\n\n                        break;\n\n                    case DnsResourceRecordType.RRSIG:\n                        List<DnsResourceRecord> recordsToRemove = new List<DnsResourceRecord>(1);\n\n                        foreach (DnsResourceRecord rrsigRecord in entry.Value)\n                        {\n                            DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;\n                            switch (rrsig.TypeCovered)\n                            {\n                                case DnsResourceRecordType.NSEC3:\n                                case DnsResourceRecordType.NSEC3PARAM:\n                                    recordsToRemove.Add(rrsigRecord);\n                                    break;\n                            }\n                        }\n\n                        if (recordsToRemove.Count > 0)\n                        {\n                            if (TryDeleteRecords(DnsResourceRecordType.RRSIG, recordsToRemove, out IReadOnlyList<DnsResourceRecord> deletedRecords))\n                                allRemovedRecords.AddRange(deletedRecords);\n                        }\n\n                        break;\n                }\n            }\n\n            return allRemovedRecords;\n        }\n\n        internal bool HasOnlyNSec3Records()\n        {\n            if (!_entries.ContainsKey(DnsResourceRecordType.NSEC3))\n                return false;\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                switch (entry.Key)\n                {\n                    case DnsResourceRecordType.NSEC3:\n                    case DnsResourceRecordType.RRSIG:\n                        break;\n\n                    default:\n                        //found non NSEC3 records\n                        return false;\n                }\n            }\n\n            return true;\n        }\n\n        internal IReadOnlyList<DnsResourceRecord> RefreshSignatures()\n        {\n            if (!_entries.TryGetValue(DnsResourceRecordType.RRSIG, out IReadOnlyList<DnsResourceRecord> rrsigRecords))\n            {\n                if ((_entries.Count == 1) && _entries.TryGetValue(DnsResourceRecordType.NS, out _))\n                    return Array.Empty<DnsResourceRecord>(); //delegation NS records are not signed\n\n                throw new InvalidOperationException();\n            }\n\n            List<DnsResourceRecordType> typesToRefresh = new List<DnsResourceRecordType>();\n            DateTime utcNow = DateTime.UtcNow;\n\n            foreach (DnsResourceRecord rrsigRecord in rrsigRecords)\n            {\n                DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;\n\n                uint signatureValidityPeriod = rrsig.SignatureExpiration - rrsig.SignatureInception;\n                uint refreshPeriod = signatureValidityPeriod / 3;\n\n                if (utcNow > DateTime.UnixEpoch.AddSeconds(rrsig.SignatureExpiration - refreshPeriod))\n                    typesToRefresh.Add(rrsig.TypeCovered);\n            }\n\n            List<DnsResourceRecord> newRRSigRecords = new List<DnsResourceRecord>(typesToRefresh.Count);\n\n            foreach (DnsResourceRecordType type in typesToRefresh)\n            {\n                if (_entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> records))\n                    newRRSigRecords.AddRange(SignRRSet(records));\n            }\n\n            return newRRSigRecords;\n        }\n\n        internal virtual IReadOnlyList<DnsResourceRecord> SignRRSet(IReadOnlyList<DnsResourceRecord> records)\n        {\n            throw new NotImplementedException();\n        }\n\n        internal IReadOnlyList<DnsResourceRecord> GetUpdatedNSecRRSet(string nextDomainName, uint ttl)\n        {\n            List<DnsResourceRecordType> types = new List<DnsResourceRecordType>(_entries.Count);\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n                types.Add(entry.Key);\n\n            if (!types.Contains(DnsResourceRecordType.NSEC))\n            {\n                types.Add(DnsResourceRecordType.NSEC);\n\n                if (!types.Contains(DnsResourceRecordType.RRSIG))\n                    types.Add(DnsResourceRecordType.RRSIG);\n            }\n\n            types.Sort();\n\n            DnsNSECRecordData newNSecRecord = new DnsNSECRecordData(nextDomainName, types);\n\n            if (!_entries.TryGetValue(DnsResourceRecordType.NSEC, out IReadOnlyList<DnsResourceRecord> existingRecords) || (existingRecords[0].TTL != ttl) || !existingRecords[0].RDATA.Equals(newNSecRecord))\n                return new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NSEC, DnsClass.IN, ttl, newNSecRecord) };\n\n            return Array.Empty<DnsResourceRecord>();\n        }\n\n        internal IReadOnlyList<DnsResourceRecord> GetUpdatedNSec3RRSet(IReadOnlyList<DnsResourceRecord> newNSec3Records)\n        {\n            if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3, out IReadOnlyList<DnsResourceRecord> existingRecords) || (existingRecords[0].TTL != newNSec3Records[0].TTL) || !existingRecords[0].RDATA.Equals(newNSec3Records[0].RDATA))\n                return newNSec3Records;\n\n            return Array.Empty<DnsResourceRecord>();\n        }\n\n        internal IReadOnlyList<DnsResourceRecord> CreateNSec3RRSet(string hashedOwnerName, byte[] nextHashedOwnerName, uint ttl, ushort iterations, byte[] salt)\n        {\n            List<DnsResourceRecordType> types = new List<DnsResourceRecordType>(_entries.Count);\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                switch (entry.Key)\n                {\n                    case DnsResourceRecordType.NSEC3:\n                        //rare case when there is a record created at the same name as that of an existing NSEC3\n                        continue;\n\n                    default:\n                        types.Add(entry.Key);\n                        break;\n                }\n            }\n\n            types.Sort();\n\n            DnsNSEC3RecordData newNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, nextHashedOwnerName, types);\n            return new DnsResourceRecord[] { new DnsResourceRecord(hashedOwnerName, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newNSec3) };\n        }\n\n        internal DnsResourceRecord GetPartialNSec3Record(string zoneName, uint ttl, ushort iterations, byte[] salt)\n        {\n            List<DnsResourceRecordType> types = new List<DnsResourceRecordType>(_entries.Count);\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                switch (entry.Key)\n                {\n                    case DnsResourceRecordType.NSEC3:\n                        //rare case when there is a record created at the same name as that of an existing NSEC3\n                        continue;\n\n                    default:\n                        types.Add(entry.Key);\n                        break;\n                }\n            }\n\n            if (_name.Equals(zoneName, StringComparison.OrdinalIgnoreCase))\n            {\n                if (!types.Contains(DnsResourceRecordType.NSEC3PARAM))\n                    types.Add(DnsResourceRecordType.NSEC3PARAM); //add NSEC3PARAM type to NSEC3 for unsigned zone apex\n            }\n\n            types.Sort();\n\n            DnsNSEC3RecordData newNSec3Record = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty<byte>(), types);\n            return new DnsResourceRecord(newNSec3Record.ComputeHashedOwnerName(_name) + (zoneName.Length > 0 ? \".\" + zoneName : \"\"), DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newNSec3Record);\n        }\n\n        #endregion\n\n        #region public\n\n        public void SyncRecords(Dictionary<DnsResourceRecordType, List<DnsResourceRecord>> newEntries)\n        {\n            //remove entires of type that do not exists in new entries\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                if (!newEntries.ContainsKey(entry.Key))\n                    _entries.TryRemove(entry.Key, out _);\n            }\n\n            //set new entries into zone\n            if (this is ForwarderZone)\n            {\n                //skip NS and SOA records from being added to ForwarderZone\n                foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> newEntry in newEntries)\n                {\n                    switch (newEntry.Key)\n                    {\n                        case DnsResourceRecordType.NS:\n                        case DnsResourceRecordType.SOA:\n                            break;\n\n                        default:\n                            _entries[newEntry.Key] = newEntry.Value;\n                            break;\n                    }\n                }\n            }\n            else\n            {\n                foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> newEntry in newEntries)\n                {\n                    if (newEntry.Key == DnsResourceRecordType.SOA)\n                    {\n                        if (newEntry.Value.Count != 1)\n                            continue; //skip invalid SOA record\n\n                        if (this is SecondaryZone)\n                        {\n                            //copy existing SOA record's info to new SOA record\n                            DnsResourceRecord existingSoaRecord = _entries[DnsResourceRecordType.SOA][0];\n                            DnsResourceRecord newSoaRecord = newEntry.Value[0];\n\n                            newSoaRecord.CopyRecordInfoFrom(existingSoaRecord);\n                        }\n                    }\n\n                    _entries[newEntry.Key] = newEntry.Value;\n                }\n            }\n        }\n\n        public void SyncRecords(Dictionary<DnsResourceRecordType, List<DnsResourceRecord>> deletedEntries, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>> addedEntries)\n        {\n            if (deletedEntries is not null)\n            {\n                foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> deletedEntry in deletedEntries)\n                {\n                    if (_entries.TryGetValue(deletedEntry.Key, out IReadOnlyList<DnsResourceRecord> existingRecords))\n                    {\n                        List<DnsResourceRecord> updatedRecords = new List<DnsResourceRecord>(Math.Max(0, existingRecords.Count - deletedEntry.Value.Count));\n\n                        foreach (DnsResourceRecord existingRecord in existingRecords)\n                        {\n                            bool deleted = false;\n\n                            foreach (DnsResourceRecord deletedRecord in deletedEntry.Value)\n                            {\n                                if (existingRecord.RDATA.Equals(deletedRecord.RDATA))\n                                {\n                                    deleted = true;\n                                    break;\n                                }\n                            }\n\n                            if (!deleted)\n                                updatedRecords.Add(existingRecord);\n                        }\n\n                        if (existingRecords.Count > updatedRecords.Count)\n                        {\n                            if (updatedRecords.Count > 0)\n                                _entries[deletedEntry.Key] = updatedRecords;\n                            else\n                                _entries.TryRemove(deletedEntry.Key, out _);\n                        }\n                    }\n                }\n            }\n\n            if (addedEntries is not null)\n            {\n                foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> addedEntry in addedEntries)\n                {\n                    _entries.AddOrUpdate(addedEntry.Key, addedEntry.Value, delegate (DnsResourceRecordType key, IReadOnlyList<DnsResourceRecord> existingRecords)\n                    {\n                        List<DnsResourceRecord> updatedRecords = new List<DnsResourceRecord>(existingRecords.Count + addedEntry.Value.Count);\n\n                        updatedRecords.AddRange(existingRecords);\n\n                        foreach (DnsResourceRecord addedRecord in addedEntry.Value)\n                        {\n                            bool exists = false;\n\n                            foreach (DnsResourceRecord existingRecord in existingRecords)\n                            {\n                                if (addedRecord.RDATA.Equals(existingRecord.RDATA))\n                                {\n                                    exists = true;\n                                    break;\n                                }\n                            }\n\n                            if (!exists)\n                                updatedRecords.Add(addedRecord);\n                        }\n\n                        if (updatedRecords.Count > existingRecords.Count)\n                            return updatedRecords;\n                        else\n                            return existingRecords;\n                    });\n                }\n            }\n        }\n\n        public void SyncGlueRecords(IReadOnlyCollection<DnsResourceRecord> deletedGlueRecords, IReadOnlyCollection<DnsResourceRecord> addedGlueRecords)\n        {\n            if (_entries.TryGetValue(DnsResourceRecordType.NS, out IReadOnlyList<DnsResourceRecord> nsRecords))\n            {\n                foreach (DnsResourceRecord nsRecord in nsRecords)\n                    nsRecord.SyncGlueRecords(deletedGlueRecords, addedGlueRecords);\n            }\n        }\n\n        public void LoadRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            _entries[type] = records;\n        }\n\n        public virtual void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.CNAME:\n                case DnsResourceRecordType.DNAME:\n                case DnsResourceRecordType.APP:\n                    if ((!_entries.IsEmpty) && !_entries.ContainsKey(type))\n                        throw new InvalidOperationException($\"Cannot add record: {type} record already exists for the same name.\");\n\n                    break;\n\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.RRSIG:\n                    break; //ignore\n\n                default:\n                    if (_entries.ContainsKey(DnsResourceRecordType.CNAME))\n                        throw new InvalidOperationException(\"Cannot add record: a CNAME record cannot exists with other record types for the same name.\");\n\n                    break;\n            }\n\n            _entries[type] = records;\n        }\n\n        public virtual bool AddRecord(DnsResourceRecord record)\n        {\n            AddRecord(record, out IReadOnlyList<DnsResourceRecord> addedRecords, out _);\n\n            return addedRecords.Count > 0;\n        }\n\n        public virtual bool DeleteRecords(DnsResourceRecordType type)\n        {\n            return _entries.TryRemove(type, out _);\n        }\n\n        public virtual bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata)\n        {\n            return TryDeleteRecord(type, rdata, out _);\n        }\n\n        public virtual void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            if (oldRecord.Type == DnsResourceRecordType.SOA)\n                throw new InvalidOperationException(\"Cannot update record: use SetRecords() for \" + oldRecord.Type.ToString() + \" record\");\n\n            if (oldRecord.Type != newRecord.Type)\n                throw new InvalidOperationException(\"Old and new record types do not match.\");\n\n            if (!DeleteRecord(oldRecord.Type, oldRecord.RDATA))\n                throw new DnsWebServiceException(\"Cannot update record: the old record does not exists.\");\n\n            AddRecord(newRecord);\n        }\n\n        public virtual IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.APP:\n                case DnsResourceRecordType.FWD:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3:\n                    {\n                        //return only exact type if exists\n                        if (_entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n                        {\n                            IReadOnlyList<DnsResourceRecord> filteredRecords = FilterDisabledRecords(type, existingRecords);\n                            if (filteredRecords.Count > 0)\n                            {\n                                if (dnssecOk)\n                                    return AppendRRSigTo(filteredRecords);\n\n                                return filteredRecords;\n                            }\n                        }\n                    }\n                    break;\n\n                case DnsResourceRecordType.ANY:\n                    List<DnsResourceRecord> records = new List<DnsResourceRecord>(_entries.Count * 2);\n\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n                    {\n                        switch (entry.Key)\n                        {\n                            case DnsResourceRecordType.FWD:\n                            case DnsResourceRecordType.APP:\n                                //skip records\n                                continue;\n\n                            default:\n                                records.AddRange(entry.Value);\n                                break;\n                        }\n                    }\n\n                    return FilterDisabledRecords(type, records);\n\n                default:\n                    {\n                        //check for CNAME\n                        if (_entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList<DnsResourceRecord> existingCNAMERecords))\n                        {\n                            IReadOnlyList<DnsResourceRecord> filteredRecords = FilterDisabledRecords(type, existingCNAMERecords);\n                            if (filteredRecords.Count > 0)\n                            {\n                                if (dnssecOk)\n                                    return AppendRRSigTo(filteredRecords);\n\n                                return filteredRecords;\n                            }\n                        }\n\n                        //check for exact type\n                        if (_entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n                        {\n                            IReadOnlyList<DnsResourceRecord> filteredRecords = FilterDisabledRecords(type, existingRecords);\n                            if (filteredRecords.Count > 0)\n                            {\n                                if (dnssecOk)\n                                    return AppendRRSigTo(filteredRecords);\n\n                                return filteredRecords;\n                            }\n                        }\n\n                        //check special processing\n                        switch (type)\n                        {\n                            case DnsResourceRecordType.A:\n                            case DnsResourceRecordType.AAAA:\n                                //check for ANAME\n                                if (_entries.TryGetValue(DnsResourceRecordType.ANAME, out IReadOnlyList<DnsResourceRecord> anameRecords))\n                                    return FilterDisabledRecords(type, anameRecords);\n\n                                //check for ALIAS\n                                if (_entries.TryGetValue(DnsResourceRecordType.ALIAS, out IReadOnlyList<DnsResourceRecord> aliasRecords))\n                                {\n                                    List<DnsResourceRecord> newAliasRecords = new List<DnsResourceRecord>(aliasRecords.Count);\n\n                                    foreach (DnsResourceRecord aliasRecord in aliasRecords)\n                                    {\n                                        if ((aliasRecord.RDATA is DnsALIASRecordData alias) && (alias.Type == type))\n                                            newAliasRecords.Add(aliasRecord);\n                                    }\n\n                                    if (newAliasRecords.Count > 0)\n                                        return FilterDisabledRecords(type, newAliasRecords);\n                                }\n\n                                break;\n                        }\n                    }\n                    break;\n            }\n\n            return Array.Empty<DnsResourceRecord>();\n        }\n\n        public IReadOnlyList<DnsResourceRecord> QueryRecordsWildcard(DnsResourceRecordType type, bool dnssecOk, string queryDomain)\n        {\n            IReadOnlyList<DnsResourceRecord> answers = QueryRecords(type, dnssecOk);\n\n            if ((answers.Count > 0) && _name.StartsWith('*') && !_name.Equals(queryDomain, StringComparison.OrdinalIgnoreCase))\n            {\n                //wildcard zone; generate new answer records\n                DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answers.Count];\n\n                for (int i = 0; i < answers.Count; i++)\n                    wildcardAnswers[i] = new DnsResourceRecord(queryDomain, answers[i].Type, answers[i].Class, answers[i].TTL, answers[i].RDATA) { Tag = answers[i].Tag };\n\n                answers = wildcardAnswers;\n            }\n\n            return answers;\n        }\n\n        public IReadOnlyList<DnsResourceRecord> GetRecords(DnsResourceRecordType type)\n        {\n            if (_entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> records))\n                return records;\n\n            return Array.Empty<DnsResourceRecord>();\n        }\n\n        public override bool ContainsNameServerRecords()\n        {\n            if (!_entries.TryGetValue(DnsResourceRecordType.NS, out IReadOnlyList<DnsResourceRecord> records))\n                return false;\n\n            foreach (DnsResourceRecord record in records)\n            {\n                if (record.GetAuthGenericRecordInfo().Disabled)\n                    continue;\n\n                return true;\n            }\n\n            return false;\n        }\n\n        #endregion\n\n        #region properties\n\n        public IReadOnlyDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> Entries\n        { get { return _entries; } }\n\n        public virtual bool Disabled\n        {\n            get { return _disabled; }\n            set { _disabled = value; }\n        }\n\n        public virtual bool IsActive\n        {\n            get { return !_disabled; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/AuthZoneInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Dnssec;\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    public enum AuthZoneType : byte\n    {\n        Unknown = 0,\n        Primary = 1,\n        Secondary = 2,\n        Stub = 3,\n        Forwarder = 4,\n        SecondaryForwarder = 5,\n        Catalog = 6,\n        SecondaryCatalog = 7\n    }\n\n    public sealed class AuthZoneInfo : IComparable<AuthZoneInfo>\n    {\n        #region variables\n\n        readonly ApexZone _apexZone;\n\n        readonly string _name;\n        readonly AuthZoneType _type;\n        readonly DateTime _lastModified;\n        readonly bool _disabled;\n\n        readonly string _catalogZoneName;\n        readonly bool _overrideCatalogQueryAccess;\n        readonly bool _overrideCatalogZoneTransfer;\n        readonly bool _overrideCatalogNotify;\n        readonly bool _overrideCatalogPrimaryNameServers; //only for secondary zones\n\n        readonly AuthZoneQueryAccess _queryAccess;\n        readonly IReadOnlyCollection<NetworkAccessControl> _queryAccessNetworkACL;\n\n        readonly AuthZoneTransfer _zoneTransfer;\n        readonly IReadOnlyCollection<NetworkAccessControl> _zoneTransferNetworkACL;\n        readonly IReadOnlySet<string> _zoneTransferTsigKeyNames;\n        readonly IReadOnlyList<DnsResourceRecord> _zoneHistory; //for IXFR support\n\n        readonly AuthZoneNotify _notify;\n        readonly IReadOnlyCollection<IPAddress> _notifyNameServers;\n        readonly IReadOnlyCollection<IPAddress> _notifySecondaryCatalogNameServers;\n\n        readonly AuthZoneUpdate _update;\n        readonly IReadOnlyCollection<NetworkAccessControl> _updateNetworkACL;\n        readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> _updateSecurityPolicies;\n\n        readonly IReadOnlyCollection<DnssecPrivateKey> _dnssecPrivateKeys; //only for primary zones\n\n        readonly IReadOnlyList<NameServerAddress> _primaryNameServerAddresses; //only for secondary and stub zones\n        readonly DnsTransportProtocol _primaryZoneTransferProtocol; //only for secondary zones\n        readonly string _primaryZoneTransferTsigKeyName; //only for secondary zones\n\n        readonly DateTime _expiry; //only for secondary and stub zones\n\n        readonly bool _validateZone; //only for secondary zones\n        readonly bool _validationFailed; //only for secondary zones\n\n        #endregion\n\n        #region constructor\n\n        public AuthZoneInfo(string name, AuthZoneType type, bool disabled)\n        {\n            _name = name;\n            _type = type;\n            _lastModified = DateTime.UtcNow;\n            _disabled = disabled;\n            _queryAccess = AuthZoneQueryAccess.Allow;\n\n            switch (_type)\n            {\n                case AuthZoneType.Primary:\n                    _zoneTransfer = AuthZoneTransfer.AllowOnlyZoneNameServers;\n                    _notify = AuthZoneNotify.ZoneNameServers;\n                    _update = AuthZoneUpdate.Deny;\n                    break;\n\n                default:\n                    _zoneTransfer = AuthZoneTransfer.Deny;\n                    _notify = AuthZoneNotify.None;\n                    _update = AuthZoneUpdate.Deny;\n                    break;\n            }\n        }\n\n        public AuthZoneInfo(BinaryReader bR, DateTime lastModified)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                case 2:\n                case 3:\n                case 4:\n                case 5:\n                case 6:\n                case 7:\n                case 8:\n                case 9:\n                case 10:\n                case 11:\n                    {\n                        _name = bR.ReadShortString();\n                        _type = (AuthZoneType)bR.ReadByte();\n                        _disabled = bR.ReadBoolean();\n\n                        _queryAccess = AuthZoneQueryAccess.Allow;\n\n                        if (version >= 2)\n                        {\n                            {\n                                _zoneTransfer = (AuthZoneTransfer)bR.ReadByte();\n\n                                int count = bR.ReadByte();\n                                if (count > 0)\n                                {\n                                    NetworkAddress[] networks = new NetworkAddress[count];\n\n                                    if (version >= 9)\n                                    {\n                                        for (int i = 0; i < count; i++)\n                                            networks[i] = NetworkAddress.ReadFrom(bR);\n                                    }\n                                    else\n                                    {\n                                        for (int i = 0; i < count; i++)\n                                        {\n                                            IPAddress address = IPAddressExtensions.ReadFrom(bR);\n\n                                            switch (address.AddressFamily)\n                                            {\n                                                case AddressFamily.InterNetwork:\n                                                    networks[i] = new NetworkAddress(address, 32);\n                                                    break;\n\n                                                case AddressFamily.InterNetworkV6:\n                                                    networks[i] = new NetworkAddress(address, 128);\n                                                    break;\n\n                                                default:\n                                                    throw new InvalidOperationException();\n                                            }\n                                        }\n                                    }\n\n                                    _zoneTransferNetworkACL = ConvertDenyAllowToACL(null, networks);\n                                }\n                            }\n\n                            {\n                                _notify = (AuthZoneNotify)bR.ReadByte();\n\n                                int count = bR.ReadByte();\n                                if (count > 0)\n                                {\n                                    IPAddress[] nameServers = new IPAddress[count];\n\n                                    for (int i = 0; i < count; i++)\n                                        nameServers[i] = IPAddressExtensions.ReadFrom(bR);\n\n                                    _notifyNameServers = nameServers;\n                                }\n                            }\n\n                            if (version >= 6)\n                            {\n                                _update = (AuthZoneUpdate)bR.ReadByte();\n\n                                int count = bR.ReadByte();\n                                if (count > 0)\n                                {\n                                    NetworkAddress[] networks = new NetworkAddress[count];\n\n                                    if (version >= 9)\n                                    {\n                                        for (int i = 0; i < count; i++)\n                                            networks[i] = NetworkAddress.ReadFrom(bR);\n                                    }\n                                    else\n                                    {\n                                        for (int i = 0; i < count; i++)\n                                        {\n                                            IPAddress address = IPAddressExtensions.ReadFrom(bR);\n\n                                            switch (address.AddressFamily)\n                                            {\n                                                case AddressFamily.InterNetwork:\n                                                    networks[i] = new NetworkAddress(address, 32);\n                                                    break;\n\n                                                case AddressFamily.InterNetworkV6:\n                                                    networks[i] = new NetworkAddress(address, 128);\n                                                    break;\n\n                                                default:\n                                                    throw new InvalidOperationException();\n                                            }\n                                        }\n                                    }\n\n                                    _updateNetworkACL = ConvertDenyAllowToACL(null, networks);\n                                }\n                            }\n                        }\n                        else\n                        {\n                            switch (_type)\n                            {\n                                case AuthZoneType.Primary:\n                                    _zoneTransfer = AuthZoneTransfer.AllowOnlyZoneNameServers;\n                                    _notify = AuthZoneNotify.ZoneNameServers;\n                                    _update = AuthZoneUpdate.Deny;\n                                    break;\n\n                                default:\n                                    _zoneTransfer = AuthZoneTransfer.Deny;\n                                    _notify = AuthZoneNotify.None;\n                                    _update = AuthZoneUpdate.Deny;\n                                    break;\n                            }\n                        }\n\n                        if (version >= 8)\n                            _lastModified = bR.ReadDateTime();\n                        else\n                            _lastModified = lastModified;\n\n                        switch (_type)\n                        {\n                            case AuthZoneType.Primary:\n                                {\n                                    if (version >= 3)\n                                    {\n                                        int count = bR.ReadInt32();\n                                        DnsResourceRecord[] zoneHistory = new DnsResourceRecord[count];\n\n                                        if (version >= 11)\n                                        {\n                                            for (int i = 0; i < count; i++)\n                                            {\n                                                zoneHistory[i] = new DnsResourceRecord(bR.BaseStream);\n\n                                                if (bR.ReadBoolean())\n                                                    zoneHistory[i].Tag = new HistoryRecordInfo(bR);\n                                            }\n                                        }\n                                        else\n                                        {\n                                            for (int i = 0; i < count; i++)\n                                            {\n                                                zoneHistory[i] = new DnsResourceRecord(bR.BaseStream);\n                                                zoneHistory[i].Tag = new HistoryRecordInfo(bR);\n                                            }\n                                        }\n\n                                        _zoneHistory = zoneHistory;\n                                    }\n\n                                    if (version >= 4)\n                                    {\n                                        int count = bR.ReadByte();\n                                        HashSet<string> tsigKeyNames = new HashSet<string>(count);\n\n                                        for (int i = 0; i < count; i++)\n                                            tsigKeyNames.Add(bR.ReadShortString());\n\n                                        _zoneTransferTsigKeyNames = tsigKeyNames;\n                                    }\n\n                                    if (version >= 7)\n                                    {\n                                        int count = bR.ReadByte();\n                                        Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicies = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>>(count);\n\n                                        for (int i = 0; i < count; i++)\n                                        {\n                                            string tsigKeyName = bR.ReadShortString().ToLowerInvariant();\n\n                                            if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>> policyMap))\n                                            {\n                                                policyMap = new Dictionary<string, IReadOnlyList<DnsResourceRecordType>>();\n                                                updateSecurityPolicies.Add(tsigKeyName, policyMap);\n                                            }\n\n                                            int policyCount = bR.ReadByte();\n\n                                            for (int j = 0; j < policyCount; j++)\n                                            {\n                                                string domain = bR.ReadShortString().ToLowerInvariant();\n\n                                                if (!policyMap.TryGetValue(domain, out IReadOnlyList<DnsResourceRecordType> types))\n                                                {\n                                                    types = new List<DnsResourceRecordType>();\n                                                    (policyMap as Dictionary<string, IReadOnlyList<DnsResourceRecordType>>).Add(domain, types);\n                                                }\n\n                                                int typeCount = bR.ReadByte();\n\n                                                for (int k = 0; k < typeCount; k++)\n                                                    (types as List<DnsResourceRecordType>).Add((DnsResourceRecordType)bR.ReadUInt16());\n                                            }\n                                        }\n\n                                        _updateSecurityPolicies = updateSecurityPolicies;\n                                    }\n                                    else if (version >= 6)\n                                    {\n                                        int count = bR.ReadByte();\n                                        Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicies = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>>(count);\n\n                                        Dictionary<string, IReadOnlyList<DnsResourceRecordType>> defaultAllowPolicy = new Dictionary<string, IReadOnlyList<DnsResourceRecordType>>(1);\n                                        defaultAllowPolicy.Add(_name, new List<DnsResourceRecordType>() { DnsResourceRecordType.ANY });\n                                        defaultAllowPolicy.Add(\"*.\" + _name, new List<DnsResourceRecordType>() { DnsResourceRecordType.ANY });\n\n                                        for (int i = 0; i < count; i++)\n                                            updateSecurityPolicies.Add(bR.ReadShortString().ToLowerInvariant(), defaultAllowPolicy);\n\n                                        _updateSecurityPolicies = updateSecurityPolicies;\n                                    }\n\n                                    if (version >= 5)\n                                    {\n                                        int count = bR.ReadByte();\n                                        if (count > 0)\n                                        {\n                                            List<DnssecPrivateKey> dnssecPrivateKeys = new List<DnssecPrivateKey>(count);\n\n                                            for (int i = 0; i < count; i++)\n                                                dnssecPrivateKeys.Add(DnssecPrivateKey.ReadFrom(bR));\n\n                                            _dnssecPrivateKeys = dnssecPrivateKeys;\n                                        }\n                                    }\n                                }\n                                break;\n\n                            case AuthZoneType.Secondary:\n                                {\n                                    _expiry = bR.ReadDateTime();\n\n                                    if (version >= 4)\n                                    {\n                                        int count = bR.ReadInt32();\n                                        DnsResourceRecord[] zoneHistory = new DnsResourceRecord[count];\n\n                                        if (version >= 11)\n                                        {\n                                            for (int i = 0; i < count; i++)\n                                            {\n                                                zoneHistory[i] = new DnsResourceRecord(bR.BaseStream);\n\n                                                if (bR.ReadBoolean())\n                                                    zoneHistory[i].Tag = new HistoryRecordInfo(bR);\n                                            }\n                                        }\n                                        else\n                                        {\n                                            for (int i = 0; i < count; i++)\n                                            {\n                                                zoneHistory[i] = new DnsResourceRecord(bR.BaseStream);\n                                                zoneHistory[i].Tag = new HistoryRecordInfo(bR);\n                                            }\n                                        }\n\n                                        _zoneHistory = zoneHistory;\n                                    }\n\n                                    if (version >= 4)\n                                    {\n                                        int count = bR.ReadByte();\n                                        HashSet<string> tsigKeyNames = new HashSet<string>(count);\n\n                                        for (int i = 0; i < count; i++)\n                                            tsigKeyNames.Add(bR.ReadShortString());\n\n                                        _zoneTransferTsigKeyNames = tsigKeyNames;\n                                    }\n\n                                    if (version == 6)\n                                    {\n                                        //MUST skip old version data\n                                        int count = bR.ReadByte();\n                                        Dictionary<string, object> tsigKeyNames = new Dictionary<string, object>(count);\n\n                                        for (int i = 0; i < count; i++)\n                                            tsigKeyNames.Add(bR.ReadShortString(), null);\n                                    }\n                                }\n                                break;\n\n                            case AuthZoneType.Stub:\n                                {\n                                    _expiry = bR.ReadDateTime();\n                                }\n                                break;\n\n                            case AuthZoneType.Forwarder:\n                                {\n                                    if (version >= 10)\n                                    {\n                                        int count = bR.ReadByte();\n                                        Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicies = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>>(count);\n\n                                        for (int i = 0; i < count; i++)\n                                        {\n                                            string tsigKeyName = bR.ReadShortString().ToLowerInvariant();\n\n                                            if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>> policyMap))\n                                            {\n                                                policyMap = new Dictionary<string, IReadOnlyList<DnsResourceRecordType>>();\n                                                updateSecurityPolicies.Add(tsigKeyName, policyMap);\n                                            }\n\n                                            int policyCount = bR.ReadByte();\n\n                                            for (int j = 0; j < policyCount; j++)\n                                            {\n                                                string domain = bR.ReadShortString().ToLowerInvariant();\n\n                                                if (!policyMap.TryGetValue(domain, out IReadOnlyList<DnsResourceRecordType> types))\n                                                {\n                                                    types = new List<DnsResourceRecordType>();\n                                                    (policyMap as Dictionary<string, IReadOnlyList<DnsResourceRecordType>>).Add(domain, types);\n                                                }\n\n                                                int typeCount = bR.ReadByte();\n\n                                                for (int k = 0; k < typeCount; k++)\n                                                    (types as List<DnsResourceRecordType>).Add((DnsResourceRecordType)bR.ReadUInt16());\n                                            }\n                                        }\n\n                                        _updateSecurityPolicies = updateSecurityPolicies;\n                                    }\n                                }\n                                break;\n                        }\n                    }\n                    break;\n\n                case 12:\n                case 13:\n                case 14:\n                    {\n                        _name = bR.ReadShortString();\n                        _type = (AuthZoneType)bR.ReadByte();\n                        _lastModified = bR.ReadDateTime();\n                        _disabled = bR.ReadBoolean();\n\n                        switch (_type)\n                        {\n                            case AuthZoneType.Primary:\n                                _catalogZoneName = bR.ReadShortString();\n                                if (_catalogZoneName.Length == 0)\n                                    _catalogZoneName = null;\n\n                                _overrideCatalogQueryAccess = bR.ReadBoolean();\n                                _overrideCatalogZoneTransfer = bR.ReadBoolean();\n                                _overrideCatalogNotify = bR.ReadBoolean();\n\n                                _queryAccess = (AuthZoneQueryAccess)bR.ReadByte();\n                                _queryAccessNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _zoneTransfer = (AuthZoneTransfer)bR.ReadByte();\n                                _zoneTransferNetworkACL = ReadNetworkACLFrom(bR);\n                                _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR);\n                                _zoneHistory = ReadZoneHistoryFrom(bR);\n\n                                _notify = (AuthZoneNotify)bR.ReadByte();\n                                _notifyNameServers = ReadIPAddressesFrom(bR);\n\n                                _update = (AuthZoneUpdate)bR.ReadByte();\n                                _updateNetworkACL = ReadNetworkACLFrom(bR);\n                                _updateSecurityPolicies = ReadUpdateSecurityPoliciesFrom(bR);\n\n                                _dnssecPrivateKeys = ReadDnssecPrivateKeysFrom(bR);\n                                break;\n\n                            case AuthZoneType.Secondary:\n                                _catalogZoneName = bR.ReadShortString();\n                                if (_catalogZoneName.Length == 0)\n                                    _catalogZoneName = null;\n\n                                _overrideCatalogQueryAccess = bR.ReadBoolean();\n                                _overrideCatalogZoneTransfer = bR.ReadBoolean();\n                                _overrideCatalogPrimaryNameServers = bR.ReadBoolean();\n\n                                _queryAccess = (AuthZoneQueryAccess)bR.ReadByte();\n                                _queryAccessNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _zoneTransfer = (AuthZoneTransfer)bR.ReadByte();\n                                _zoneTransferNetworkACL = ReadNetworkACLFrom(bR);\n                                _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR);\n                                _zoneHistory = ReadZoneHistoryFrom(bR);\n\n                                _notify = (AuthZoneNotify)bR.ReadByte();\n                                _notifyNameServers = ReadIPAddressesFrom(bR);\n\n                                _update = (AuthZoneUpdate)bR.ReadByte();\n                                _updateNetworkACL = ReadNetworkACLFrom(bR);\n\n                                if (version >= 14)\n                                    _dnssecPrivateKeys = ReadDnssecPrivateKeysFrom(bR);\n\n                                _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR);\n                                _primaryZoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte();\n                                _primaryZoneTransferTsigKeyName = bR.ReadShortString();\n                                if (_primaryZoneTransferTsigKeyName.Length == 0)\n                                    _primaryZoneTransferTsigKeyName = null;\n\n                                _expiry = bR.ReadDateTime();\n                                _validateZone = bR.ReadBoolean();\n                                _validationFailed = bR.ReadBoolean();\n                                break;\n\n                            case AuthZoneType.Stub:\n                                _catalogZoneName = bR.ReadShortString();\n                                if (_catalogZoneName.Length == 0)\n                                    _catalogZoneName = null;\n\n                                _overrideCatalogQueryAccess = bR.ReadBoolean();\n\n                                _queryAccess = (AuthZoneQueryAccess)bR.ReadByte();\n                                _queryAccessNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR);\n\n                                _expiry = bR.ReadDateTime();\n                                break;\n\n                            case AuthZoneType.Forwarder:\n                                _catalogZoneName = bR.ReadShortString();\n                                if (_catalogZoneName.Length == 0)\n                                    _catalogZoneName = null;\n\n                                _overrideCatalogQueryAccess = bR.ReadBoolean();\n                                _overrideCatalogZoneTransfer = bR.ReadBoolean();\n                                _overrideCatalogNotify = bR.ReadBoolean();\n\n                                _queryAccess = (AuthZoneQueryAccess)bR.ReadByte();\n                                _queryAccessNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _zoneTransfer = (AuthZoneTransfer)bR.ReadByte();\n                                _zoneTransferNetworkACL = ReadNetworkACLFrom(bR);\n                                _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR);\n                                _zoneHistory = ReadZoneHistoryFrom(bR);\n\n                                _notify = (AuthZoneNotify)bR.ReadByte();\n                                _notifyNameServers = ReadIPAddressesFrom(bR);\n\n                                _update = (AuthZoneUpdate)bR.ReadByte();\n                                _updateNetworkACL = ReadNetworkACLFrom(bR);\n                                _updateSecurityPolicies = ReadUpdateSecurityPoliciesFrom(bR);\n                                break;\n\n                            case AuthZoneType.SecondaryForwarder:\n                                _catalogZoneName = bR.ReadShortString();\n                                if (_catalogZoneName.Length == 0)\n                                    _catalogZoneName = null;\n\n                                _overrideCatalogQueryAccess = bR.ReadBoolean();\n\n                                _queryAccess = (AuthZoneQueryAccess)bR.ReadByte();\n                                _queryAccessNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _update = (AuthZoneUpdate)bR.ReadByte();\n                                _updateNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR);\n                                _primaryZoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte();\n                                _primaryZoneTransferTsigKeyName = bR.ReadShortString();\n                                if (_primaryZoneTransferTsigKeyName.Length == 0)\n                                    _primaryZoneTransferTsigKeyName = null;\n\n                                _expiry = bR.ReadDateTime();\n                                break;\n\n                            case AuthZoneType.Catalog:\n                                _queryAccess = (AuthZoneQueryAccess)bR.ReadByte();\n                                _queryAccessNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _zoneTransfer = (AuthZoneTransfer)bR.ReadByte();\n                                _zoneTransferNetworkACL = ReadNetworkACLFrom(bR);\n                                _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR);\n                                _zoneHistory = ReadZoneHistoryFrom(bR);\n\n                                _notify = (AuthZoneNotify)bR.ReadByte();\n                                _notifyNameServers = ReadIPAddressesFrom(bR);\n\n                                if (version >= 13)\n                                    _notifySecondaryCatalogNameServers = ReadIPAddressesFrom(bR);\n\n                                break;\n\n                            case AuthZoneType.SecondaryCatalog:\n                                _queryAccess = (AuthZoneQueryAccess)bR.ReadByte();\n                                _queryAccessNetworkACL = ReadNetworkACLFrom(bR);\n\n                                _zoneTransfer = (AuthZoneTransfer)bR.ReadByte();\n                                _zoneTransferNetworkACL = ReadNetworkACLFrom(bR);\n                                _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR);\n\n                                _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR);\n                                _primaryZoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte();\n                                _primaryZoneTransferTsigKeyName = bR.ReadShortString();\n                                if (_primaryZoneTransferTsigKeyName.Length == 0)\n                                    _primaryZoneTransferTsigKeyName = null;\n\n                                _expiry = bR.ReadDateTime();\n                                break;\n                        }\n                    }\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"AuthZoneInfo format version not supported.\");\n            }\n        }\n\n        internal AuthZoneInfo(ApexZone apexZone, bool loadHistory = false)\n        {\n            _apexZone = apexZone;\n            _name = _apexZone.Name;\n            _lastModified = _apexZone.LastModified;\n            _disabled = _apexZone.Disabled;\n\n            if (_apexZone is PrimaryZone primaryZone)\n            {\n                _type = AuthZoneType.Primary;\n\n                _catalogZoneName = _apexZone.CatalogZoneName;\n                _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess;\n                _overrideCatalogZoneTransfer = _apexZone.OverrideCatalogZoneTransfer;\n                _overrideCatalogNotify = _apexZone.OverrideCatalogNotify;\n\n                _queryAccess = _apexZone.QueryAccess;\n                _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL;\n\n                _zoneTransfer = _apexZone.ZoneTransfer;\n                _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL;\n                _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames;\n\n                if (loadHistory)\n                    _zoneHistory = _apexZone.GetZoneHistory();\n\n                _notify = _apexZone.Notify;\n                _notifyNameServers = _apexZone.NotifyNameServers;\n\n                _update = _apexZone.Update;\n                _updateNetworkACL = _apexZone.UpdateNetworkACL;\n                _updateSecurityPolicies = _apexZone.UpdateSecurityPolicies;\n\n                _dnssecPrivateKeys = primaryZone.DnssecPrivateKeys;\n            }\n            else if (_apexZone is SecondaryCatalogZone secondaryCatalogZone)\n            {\n                _type = AuthZoneType.SecondaryCatalog;\n\n                _queryAccess = _apexZone.QueryAccess;\n                _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL;\n\n                _zoneTransfer = _apexZone.ZoneTransfer;\n                _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL;\n                _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames;\n\n                _primaryNameServerAddresses = secondaryCatalogZone.PrimaryNameServerAddresses;\n                _primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol;\n                _primaryZoneTransferTsigKeyName = secondaryCatalogZone.PrimaryZoneTransferTsigKeyName;\n\n                _expiry = secondaryCatalogZone.Expiry;\n            }\n            else if (_apexZone is SecondaryForwarderZone secondaryForwarderZone)\n            {\n                _type = AuthZoneType.SecondaryForwarder;\n\n                _catalogZoneName = _apexZone.CatalogZoneName;\n                _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess;\n\n                _queryAccess = _apexZone.QueryAccess;\n                _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL;\n\n                _update = _apexZone.Update;\n                _updateNetworkACL = _apexZone.UpdateNetworkACL;\n\n                _primaryNameServerAddresses = secondaryForwarderZone.PrimaryNameServerAddresses;\n                _primaryZoneTransferProtocol = secondaryForwarderZone.PrimaryZoneTransferProtocol;\n                _primaryZoneTransferTsigKeyName = secondaryForwarderZone.PrimaryZoneTransferTsigKeyName;\n\n                _expiry = secondaryForwarderZone.Expiry;\n            }\n            else if (_apexZone is SecondaryZone secondaryZone)\n            {\n                _type = AuthZoneType.Secondary;\n\n                _catalogZoneName = _apexZone.CatalogZoneName;\n                _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess;\n                _overrideCatalogZoneTransfer = _apexZone.OverrideCatalogZoneTransfer;\n                _overrideCatalogPrimaryNameServers = secondaryZone.OverrideCatalogPrimaryNameServers;\n\n                _queryAccess = _apexZone.QueryAccess;\n                _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL;\n\n                _zoneTransfer = _apexZone.ZoneTransfer;\n                _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL;\n                _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames;\n\n                if (loadHistory)\n                    _zoneHistory = _apexZone.GetZoneHistory();\n\n                _notify = _apexZone.Notify;\n                _notifyNameServers = _apexZone.NotifyNameServers;\n\n                _update = _apexZone.Update;\n                _updateNetworkACL = _apexZone.UpdateNetworkACL;\n\n                _dnssecPrivateKeys = secondaryZone.DnssecPrivateKeys;\n\n                _primaryNameServerAddresses = secondaryZone.PrimaryNameServerAddresses;\n                _primaryZoneTransferProtocol = secondaryZone.PrimaryZoneTransferProtocol;\n                _primaryZoneTransferTsigKeyName = secondaryZone.PrimaryZoneTransferTsigKeyName;\n\n                _expiry = secondaryZone.Expiry;\n                _validateZone = secondaryZone.ValidateZone;\n                _validationFailed = secondaryZone.ValidationFailed;\n            }\n            else if (_apexZone is StubZone stubZone)\n            {\n                _type = AuthZoneType.Stub;\n\n                _catalogZoneName = _apexZone.CatalogZoneName;\n                _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess;\n\n                _queryAccess = _apexZone.QueryAccess;\n                _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL;\n\n                _primaryNameServerAddresses = stubZone.PrimaryNameServerAddresses;\n\n                _expiry = stubZone.Expiry;\n            }\n            else if (_apexZone is CatalogZone)\n            {\n                _type = AuthZoneType.Catalog;\n\n                _queryAccess = _apexZone.QueryAccess;\n                _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL;\n\n                _zoneTransfer = _apexZone.ZoneTransfer;\n                _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL;\n                _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames;\n\n                if (loadHistory)\n                    _zoneHistory = _apexZone.GetZoneHistory();\n\n                _notify = _apexZone.Notify;\n                _notifyNameServers = _apexZone.NotifyNameServers;\n                _notifySecondaryCatalogNameServers = _apexZone.NotifySecondaryCatalogNameServers;\n            }\n            else if (_apexZone is ForwarderZone)\n            {\n                _type = AuthZoneType.Forwarder;\n\n                _catalogZoneName = _apexZone.CatalogZoneName;\n                _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess;\n                _overrideCatalogZoneTransfer = _apexZone.OverrideCatalogZoneTransfer;\n                _overrideCatalogNotify = _apexZone.OverrideCatalogNotify;\n\n                _queryAccess = _apexZone.QueryAccess;\n                _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL;\n\n                _zoneTransfer = _apexZone.ZoneTransfer;\n                _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL;\n                _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames;\n\n                if (loadHistory)\n                    _zoneHistory = _apexZone.GetZoneHistory();\n\n                _notify = _apexZone.Notify;\n                _notifyNameServers = _apexZone.NotifyNameServers;\n\n                _update = _apexZone.Update;\n                _updateNetworkACL = _apexZone.UpdateNetworkACL;\n                _updateSecurityPolicies = _apexZone.UpdateSecurityPolicies;\n            }\n            else\n            {\n                _type = AuthZoneType.Unknown;\n            }\n        }\n\n        #endregion\n\n        #region static\n\n        public static string GetZoneTypeName(AuthZoneType type)\n        {\n            switch (type)\n            {\n                case AuthZoneType.SecondaryForwarder:\n                    return \"Secondary Forwarder\";\n\n                case AuthZoneType.SecondaryCatalog:\n                    return \"Secondary Catalog\";\n\n                default:\n                    return type.ToString();\n            }\n        }\n\n        internal static NameServerAddress[] ReadNameServerAddressesFrom(BinaryReader bR)\n        {\n            int count = bR.ReadByte();\n            if (count < 1)\n                return null;\n\n            NameServerAddress[] nameServerAddresses = new NameServerAddress[count];\n\n            for (int i = 0; i < count; i++)\n                nameServerAddresses[i] = new NameServerAddress(bR);\n\n            return nameServerAddresses;\n        }\n\n        internal static void WriteNameServerAddressesTo(IReadOnlyCollection<NameServerAddress> nameServerAddresses, BinaryWriter bW)\n        {\n            if (nameServerAddresses is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(nameServerAddresses.Count));\n\n                foreach (NameServerAddress network in nameServerAddresses)\n                    network.WriteTo(bW);\n            }\n        }\n\n        internal static NetworkAccessControl[] ReadNetworkACLFrom(BinaryReader bR)\n        {\n            int count = bR.ReadByte();\n            if (count < 1)\n                return null;\n\n            NetworkAccessControl[] acl = new NetworkAccessControl[count];\n\n            for (int i = 0; i < count; i++)\n                acl[i] = NetworkAccessControl.ReadFrom(bR);\n\n            return acl;\n        }\n\n        internal static void WriteNetworkACLTo(IReadOnlyCollection<NetworkAccessControl> acl, BinaryWriter bW)\n        {\n            if (acl is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(acl.Count));\n\n                foreach (NetworkAccessControl nac in acl)\n                    nac.WriteTo(bW);\n            }\n        }\n\n        internal static NetworkAddress[] ReadNetworkAddressesFrom(BinaryReader bR)\n        {\n            int count = bR.ReadByte();\n            if (count < 1)\n                return null;\n\n            NetworkAddress[] networks = new NetworkAddress[count];\n\n            for (int i = 0; i < count; i++)\n                networks[i] = NetworkAddress.ReadFrom(bR);\n\n            return networks;\n        }\n\n        internal static void WriteNetworkAddressesTo(IReadOnlyCollection<NetworkAddress> networkAddresses, BinaryWriter bW)\n        {\n            if (networkAddresses is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(networkAddresses.Count));\n\n                foreach (NetworkAddress network in networkAddresses)\n                    network.WriteTo(bW);\n            }\n        }\n\n        internal static IPAddress[] ReadIPAddressesFrom(BinaryReader bR)\n        {\n            int count = bR.ReadByte();\n            if (count < 1)\n                return null;\n\n            IPAddress[] ipAddresses = new IPAddress[count];\n\n            for (int i = 0; i < count; i++)\n                ipAddresses[i] = IPAddressExtensions.ReadFrom(bR);\n\n            return ipAddresses;\n        }\n\n        internal static void WriteIPAddressesTo(IReadOnlyCollection<IPAddress> ipAddresses, BinaryWriter bW)\n        {\n            if (ipAddresses is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(ipAddresses.Count));\n\n                foreach (IPAddress ipAddress in ipAddresses)\n                    ipAddress.WriteTo(bW);\n            }\n        }\n\n        internal static List<NetworkAccessControl> ConvertDenyAllowToACL(NetworkAddress[] deniedNetworks, NetworkAddress[] allowedNetworks)\n        {\n            List<NetworkAccessControl> acl = new List<NetworkAccessControl>();\n\n            if (deniedNetworks is not null)\n            {\n                foreach (NetworkAddress network in deniedNetworks)\n                    acl.Add(new NetworkAccessControl(network, true));\n            }\n\n            if (allowedNetworks is not null)\n            {\n                foreach (NetworkAddress network in allowedNetworks)\n                    acl.Add(new NetworkAccessControl(network));\n            }\n\n            if (acl.Count > 0)\n                return acl;\n\n            return null;\n        }\n\n        private static HashSet<string> ReadZoneTransferTsigKeyNamesFrom(BinaryReader bR)\n        {\n            int count = bR.ReadByte();\n            HashSet<string> zoneTransferTsigKeyNames = new HashSet<string>(count);\n\n            for (int i = 0; i < count; i++)\n                zoneTransferTsigKeyNames.Add(bR.ReadShortString());\n\n            return zoneTransferTsigKeyNames;\n        }\n\n        private static void WriteZoneTransferTsigKeyNamesTo(IReadOnlySet<string> zoneTransferTsigKeyNames, BinaryWriter bW)\n        {\n            if (zoneTransferTsigKeyNames is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(zoneTransferTsigKeyNames.Count));\n\n                foreach (string tsigKeyName in zoneTransferTsigKeyNames)\n                    bW.WriteShortString(tsigKeyName);\n            }\n        }\n\n        private static DnsResourceRecord[] ReadZoneHistoryFrom(BinaryReader bR)\n        {\n            int count = bR.ReadInt32();\n            DnsResourceRecord[] zoneHistory = new DnsResourceRecord[count];\n\n            for (int i = 0; i < count; i++)\n            {\n                zoneHistory[i] = new DnsResourceRecord(bR.BaseStream);\n\n                if (bR.ReadBoolean())\n                    zoneHistory[i].Tag = new HistoryRecordInfo(bR);\n            }\n\n            return zoneHistory;\n        }\n\n        private static void WriteZoneHistoryTo(IReadOnlyList<DnsResourceRecord> zoneHistory, BinaryWriter bW)\n        {\n            if (zoneHistory is null)\n            {\n                bW.Write(0);\n            }\n            else\n            {\n                bW.Write(zoneHistory.Count);\n\n                foreach (DnsResourceRecord record in zoneHistory)\n                {\n                    record.WriteTo(bW.BaseStream);\n\n                    if (record.Tag is HistoryRecordInfo rrInfo)\n                    {\n                        bW.Write(true);\n                        rrInfo.WriteTo(bW);\n                    }\n                    else\n                    {\n                        bW.Write(false);\n                    }\n                }\n            }\n        }\n\n        private static Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> ReadUpdateSecurityPoliciesFrom(BinaryReader bR)\n        {\n            int count = bR.ReadInt32();\n            Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicies = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>>(count);\n\n            for (int i = 0; i < count; i++)\n            {\n                string tsigKeyName = bR.ReadShortString().ToLowerInvariant();\n\n                if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>> policyMap))\n                {\n                    policyMap = new Dictionary<string, IReadOnlyList<DnsResourceRecordType>>();\n                    updateSecurityPolicies.Add(tsigKeyName, policyMap);\n                }\n\n                int policyCount = bR.ReadByte();\n\n                for (int j = 0; j < policyCount; j++)\n                {\n                    string domain = bR.ReadShortString().ToLowerInvariant();\n\n                    if (!policyMap.TryGetValue(domain, out IReadOnlyList<DnsResourceRecordType> types))\n                    {\n                        types = new List<DnsResourceRecordType>();\n                        (policyMap as Dictionary<string, IReadOnlyList<DnsResourceRecordType>>).Add(domain, types);\n                    }\n\n                    int typeCount = bR.ReadByte();\n\n                    for (int k = 0; k < typeCount; k++)\n                        (types as List<DnsResourceRecordType>).Add((DnsResourceRecordType)bR.ReadUInt16());\n                }\n            }\n\n            return updateSecurityPolicies;\n        }\n\n        private static void WriteUpdateSecurityPoliciesTo(IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicies, BinaryWriter bW)\n        {\n            if (updateSecurityPolicies is null)\n            {\n                bW.Write(0);\n            }\n            else\n            {\n                bW.Write(updateSecurityPolicies.Count);\n\n                foreach (KeyValuePair<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicy in updateSecurityPolicies)\n                {\n                    bW.WriteShortString(updateSecurityPolicy.Key);\n                    bW.Write(Convert.ToByte(updateSecurityPolicy.Value.Count));\n\n                    foreach (KeyValuePair<string, IReadOnlyList<DnsResourceRecordType>> policyMap in updateSecurityPolicy.Value)\n                    {\n                        bW.WriteShortString(policyMap.Key);\n                        bW.Write(Convert.ToByte(policyMap.Value.Count));\n\n                        foreach (DnsResourceRecordType type in policyMap.Value)\n                            bW.Write((ushort)type);\n                    }\n                }\n            }\n        }\n\n        internal static DnssecPrivateKey[] ReadDnssecPrivateKeysFrom(BinaryReader bR)\n        {\n            int count = bR.ReadByte();\n            if (count < 1)\n                return null;\n\n            DnssecPrivateKey[] dnssecPrivateKeys = new DnssecPrivateKey[count];\n\n            for (int i = 0; i < count; i++)\n                dnssecPrivateKeys[i] = DnssecPrivateKey.ReadFrom(bR);\n\n            return dnssecPrivateKeys;\n        }\n\n        internal static void WriteDnssecPrivateKeysTo(IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys, BinaryWriter bW)\n        {\n            if (dnssecPrivateKeys is null)\n            {\n                bW.Write((byte)0);\n            }\n            else\n            {\n                bW.Write(Convert.ToByte(dnssecPrivateKeys.Count));\n\n                foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)\n                    dnssecPrivateKey.WriteTo(bW);\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public void TriggerRefresh()\n        {\n            if (_apexZone is null)\n                throw new InvalidOperationException();\n\n            switch (_type)\n            {\n                case AuthZoneType.Secondary:\n                case AuthZoneType.SecondaryForwarder:\n                case AuthZoneType.SecondaryCatalog:\n                    (_apexZone as SecondaryZone).TriggerRefresh();\n                    break;\n\n                case AuthZoneType.Stub:\n                    (_apexZone as StubZone).TriggerRefresh();\n                    break;\n\n                default:\n                    throw new InvalidOperationException();\n            }\n        }\n\n        public void TriggerResync()\n        {\n            if (_apexZone is null)\n                throw new InvalidOperationException();\n\n            switch (_type)\n            {\n                case AuthZoneType.Secondary:\n                case AuthZoneType.SecondaryForwarder:\n                case AuthZoneType.SecondaryCatalog:\n                    (_apexZone as SecondaryZone).TriggerResync();\n                    break;\n\n                case AuthZoneType.Stub:\n                    (_apexZone as StubZone).TriggerResync();\n                    break;\n\n                default:\n                    throw new InvalidOperationException();\n            }\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            if (_apexZone is null)\n                throw new InvalidOperationException();\n\n            bW.Write((byte)14); //version\n\n            bW.WriteShortString(_name);\n            bW.Write((byte)_type);\n            bW.Write(_lastModified);\n            bW.Write(_disabled);\n\n            switch (_type)\n            {\n                case AuthZoneType.Primary:\n                    bW.Write(_catalogZoneName ?? \"\");\n                    bW.Write(_overrideCatalogQueryAccess);\n                    bW.Write(_overrideCatalogZoneTransfer);\n                    bW.Write(_overrideCatalogNotify);\n\n                    bW.Write((byte)_queryAccess);\n                    WriteNetworkACLTo(_queryAccessNetworkACL, bW);\n\n                    bW.Write((byte)_zoneTransfer);\n                    WriteNetworkACLTo(_zoneTransferNetworkACL, bW);\n                    WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW);\n                    WriteZoneHistoryTo(_zoneHistory, bW);\n\n                    bW.Write((byte)_notify);\n                    WriteIPAddressesTo(_notifyNameServers, bW);\n\n                    bW.Write((byte)_update);\n                    WriteNetworkACLTo(_updateNetworkACL, bW);\n                    WriteUpdateSecurityPoliciesTo(_updateSecurityPolicies, bW);\n\n                    WriteDnssecPrivateKeysTo(_dnssecPrivateKeys, bW);\n                    break;\n\n                case AuthZoneType.Secondary:\n                    bW.Write(_catalogZoneName ?? \"\");\n                    bW.Write(_overrideCatalogQueryAccess);\n                    bW.Write(_overrideCatalogZoneTransfer);\n                    bW.Write(_overrideCatalogPrimaryNameServers);\n\n                    bW.Write((byte)_queryAccess);\n                    WriteNetworkACLTo(_queryAccessNetworkACL, bW);\n\n                    bW.Write((byte)_zoneTransfer);\n                    WriteNetworkACLTo(_zoneTransferNetworkACL, bW);\n                    WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW);\n                    WriteZoneHistoryTo(_zoneHistory, bW);\n\n                    bW.Write((byte)_notify);\n                    WriteIPAddressesTo(_notifyNameServers, bW);\n\n                    bW.Write((byte)_update);\n                    WriteNetworkACLTo(_updateNetworkACL, bW);\n\n                    WriteDnssecPrivateKeysTo(_dnssecPrivateKeys, bW);\n\n                    WriteNameServerAddressesTo(_primaryNameServerAddresses, bW);\n                    bW.Write((byte)_primaryZoneTransferProtocol);\n                    bW.Write(_primaryZoneTransferTsigKeyName ?? \"\");\n\n                    bW.Write(_expiry);\n                    bW.Write(_validateZone);\n                    bW.Write(_validationFailed);\n                    break;\n\n                case AuthZoneType.Stub:\n                    bW.Write(_catalogZoneName ?? \"\");\n                    bW.Write(_overrideCatalogQueryAccess);\n\n                    bW.Write((byte)_queryAccess);\n                    WriteNetworkACLTo(_queryAccessNetworkACL, bW);\n\n                    WriteNameServerAddressesTo(_primaryNameServerAddresses, bW);\n\n                    bW.Write(_expiry);\n                    break;\n\n                case AuthZoneType.Forwarder:\n                    bW.Write(_catalogZoneName ?? \"\");\n                    bW.Write(_overrideCatalogQueryAccess);\n                    bW.Write(_overrideCatalogZoneTransfer);\n                    bW.Write(_overrideCatalogNotify);\n\n                    bW.Write((byte)_queryAccess);\n                    WriteNetworkACLTo(_queryAccessNetworkACL, bW);\n\n                    bW.Write((byte)_zoneTransfer);\n                    WriteNetworkACLTo(_zoneTransferNetworkACL, bW);\n                    WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW);\n                    WriteZoneHistoryTo(_zoneHistory, bW);\n\n                    bW.Write((byte)_notify);\n                    WriteIPAddressesTo(_notifyNameServers, bW);\n\n                    bW.Write((byte)_update);\n                    WriteNetworkACLTo(_updateNetworkACL, bW);\n                    WriteUpdateSecurityPoliciesTo(_updateSecurityPolicies, bW);\n                    break;\n\n                case AuthZoneType.SecondaryForwarder:\n                    bW.Write(_catalogZoneName ?? \"\");\n                    bW.Write(_overrideCatalogQueryAccess);\n\n                    bW.Write((byte)_queryAccess);\n                    WriteNetworkACLTo(_queryAccessNetworkACL, bW);\n\n                    bW.Write((byte)_update);\n                    WriteNetworkACLTo(_updateNetworkACL, bW);\n\n                    WriteNameServerAddressesTo(_primaryNameServerAddresses, bW);\n                    bW.Write((byte)_primaryZoneTransferProtocol);\n                    bW.Write(_primaryZoneTransferTsigKeyName ?? \"\");\n\n                    bW.Write(_expiry);\n                    break;\n\n                case AuthZoneType.Catalog:\n                    bW.Write((byte)_queryAccess);\n                    WriteNetworkACLTo(_queryAccessNetworkACL, bW);\n\n                    bW.Write((byte)_zoneTransfer);\n                    WriteNetworkACLTo(_zoneTransferNetworkACL, bW);\n                    WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW);\n                    WriteZoneHistoryTo(_zoneHistory, bW);\n\n                    bW.Write((byte)_notify);\n                    WriteIPAddressesTo(_notifyNameServers, bW);\n                    WriteIPAddressesTo(_notifySecondaryCatalogNameServers, bW);\n                    break;\n\n                case AuthZoneType.SecondaryCatalog:\n                    bW.Write((byte)_queryAccess);\n                    WriteNetworkACLTo(_queryAccessNetworkACL, bW);\n\n                    bW.Write((byte)_zoneTransfer);\n                    WriteNetworkACLTo(_zoneTransferNetworkACL, bW);\n                    WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW);\n\n                    WriteNameServerAddressesTo(_primaryNameServerAddresses, bW);\n                    bW.Write((byte)_primaryZoneTransferProtocol);\n                    bW.Write(_primaryZoneTransferTsigKeyName ?? \"\");\n\n                    bW.Write(_expiry);\n                    break;\n            }\n        }\n\n        public int CompareTo(AuthZoneInfo other)\n        {\n            return _name.CompareTo(other._name);\n        }\n\n        public override bool Equals(object obj)\n        {\n            if (ReferenceEquals(this, obj))\n                return true;\n\n            if (obj is not AuthZoneInfo other)\n                return false;\n\n            return _name.Equals(other._name, StringComparison.OrdinalIgnoreCase);\n        }\n\n        public override int GetHashCode()\n        {\n            return HashCode.Combine(_name);\n        }\n\n        public override string ToString()\n        {\n            return _name.Length == 0 ? \"<root>\" : _name; ;\n        }\n\n        #endregion\n\n        #region properties\n\n        internal ApexZone ApexZone\n        { get { return _apexZone; } }\n\n        public string Name\n        { get { return _name; } }\n\n        public string DisplayName\n        { get { return _name.Length == 0 ? \"<root>\" : _name; } }\n\n        public AuthZoneType Type\n        { get { return _type; } }\n\n        public string TypeName\n        { get { return GetZoneTypeName(_type); } }\n\n        public DateTime LastModified\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _lastModified;\n\n                return _apexZone.LastModified;\n            }\n        }\n\n        public bool Disabled\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _disabled;\n\n                return _apexZone.Disabled;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.Disabled = value;\n            }\n        }\n\n        public string CatalogZoneName\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _catalogZoneName;\n\n                return _apexZone.CatalogZoneName;\n            }\n        }\n\n        public bool OverrideCatalogQueryAccess\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _overrideCatalogQueryAccess;\n\n                return _apexZone.OverrideCatalogQueryAccess;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.OverrideCatalogQueryAccess = value;\n            }\n        }\n\n        public bool OverrideCatalogZoneTransfer\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _overrideCatalogZoneTransfer;\n\n                return _apexZone.OverrideCatalogZoneTransfer;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.OverrideCatalogZoneTransfer = value;\n            }\n        }\n\n        public bool OverrideCatalogNotify\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _overrideCatalogNotify;\n\n                return _apexZone.OverrideCatalogNotify;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.OverrideCatalogNotify = value;\n            }\n        }\n\n        public bool OverrideCatalogPrimaryNameServers\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _overrideCatalogPrimaryNameServers;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                        return (_apexZone as SecondaryZone).OverrideCatalogPrimaryNameServers;\n\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        return false;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                        (_apexZone as SecondaryZone).OverrideCatalogPrimaryNameServers = value;\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public AuthZoneQueryAccess QueryAccess\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _queryAccess;\n\n                return _apexZone.QueryAccess;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.QueryAccess = value;\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> QueryAccessNetworkACL\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _queryAccessNetworkACL;\n\n                return _apexZone.QueryAccessNetworkACL;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.QueryAccessNetworkACL = value;\n            }\n        }\n\n        public AuthZoneTransfer ZoneTransfer\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _zoneTransfer;\n\n                return _apexZone.ZoneTransfer;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.ZoneTransfer = value;\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> ZoneTransferNetworkACL\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _zoneTransferNetworkACL;\n\n                return _apexZone.ZoneTransferNetworkACL;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.ZoneTransferNetworkACL = value;\n            }\n        }\n\n        public IReadOnlySet<string> ZoneTransferTsigKeyNames\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _zoneTransferTsigKeyNames;\n\n                return _apexZone.ZoneTransferTsigKeyNames;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                        _apexZone.ZoneTransferTsigKeyNames = value;\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public IReadOnlyList<DnsResourceRecord> ZoneHistory\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _zoneHistory;\n\n                return _apexZone.GetZoneHistory();\n            }\n        }\n\n        public AuthZoneNotify Notify\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _notify;\n\n                return _apexZone.Notify;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.Notify = value;\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> NotifyNameServers\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _notifyNameServers;\n\n                return _apexZone.NotifyNameServers;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.NotifyNameServers = value;\n            }\n        }\n\n        public IReadOnlyCollection<IPAddress> NotifySecondaryCatalogNameServers\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _notifySecondaryCatalogNameServers;\n\n                return _apexZone.NotifySecondaryCatalogNameServers;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.NotifySecondaryCatalogNameServers = value;\n            }\n        }\n\n        public AuthZoneUpdate Update\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _update;\n\n                return _apexZone.Update;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.Update = value;\n            }\n        }\n\n        public IReadOnlyCollection<NetworkAccessControl> UpdateNetworkACL\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _updateNetworkACL;\n\n                return _apexZone.UpdateNetworkACL;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                _apexZone.UpdateNetworkACL = value;\n            }\n        }\n\n        public IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> UpdateSecurityPolicies\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _updateSecurityPolicies;\n\n                return _apexZone.UpdateSecurityPolicies;\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Forwarder:\n                        _apexZone.UpdateSecurityPolicies = value;\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public IReadOnlyCollection<DnssecPrivateKey> DnssecPrivateKeys\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _dnssecPrivateKeys;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Primary:\n                        return (_apexZone as PrimaryZone).DnssecPrivateKeys;\n\n                    case AuthZoneType.Secondary:\n                        return (_apexZone as SecondaryZone).DnssecPrivateKeys;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public IReadOnlyList<NameServerAddress> PrimaryNameServerAddresses\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _primaryNameServerAddresses;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        return (_apexZone as SecondaryZone).PrimaryNameServerAddresses;\n\n                    case AuthZoneType.Stub:\n                        return (_apexZone as StubZone).PrimaryNameServerAddresses;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        (_apexZone as SecondaryZone).PrimaryNameServerAddresses = value;\n                        break;\n\n                    case AuthZoneType.Stub:\n                        (_apexZone as StubZone).PrimaryNameServerAddresses = value;\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public DnsTransportProtocol PrimaryZoneTransferProtocol\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _primaryZoneTransferProtocol;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        return (_apexZone as SecondaryZone).PrimaryZoneTransferProtocol;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        (_apexZone as SecondaryZone).PrimaryZoneTransferProtocol = value;\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public string PrimaryZoneTransferTsigKeyName\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _primaryZoneTransferTsigKeyName;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        return (_apexZone as SecondaryZone).PrimaryZoneTransferTsigKeyName;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        (_apexZone as SecondaryZone).PrimaryZoneTransferTsigKeyName = value;\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public DateTime Expiry\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _expiry;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        return (_apexZone as SecondaryZone).Expiry;\n\n                    case AuthZoneType.Stub:\n                        return (_apexZone as StubZone).Expiry;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public bool ValidateZone\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _validateZone;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                        return (_apexZone as SecondaryZone).ValidateZone;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n            set\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                        (_apexZone as SecondaryZone).ValidateZone = value;\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public bool ValidationFailed\n        {\n            get\n            {\n                if (_apexZone is null)\n                    return _validationFailed;\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                        return (_apexZone as SecondaryZone).ValidationFailed;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public uint DnsKeyTtl\n        {\n            get\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Primary:\n                        return (_apexZone as PrimaryZone).GetDnsKeyTtl();\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public bool Internal\n        {\n            get\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Primary:\n                        return (_apexZone as PrimaryZone).Internal;\n\n                    default:\n                        return false;\n                }\n            }\n        }\n\n        public bool IsExpired\n        {\n            get\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        return (_apexZone as SecondaryZone).IsExpired;\n\n                    case AuthZoneType.Stub:\n                        return (_apexZone as StubZone).IsExpired;\n\n                    default:\n                        return false;\n                }\n            }\n        }\n\n        public string[] NotifyFailed\n        {\n            get\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                        return _apexZone.NotifyFailed;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        public bool SyncFailed\n        {\n            get\n            {\n                if (_apexZone is null)\n                    throw new InvalidOperationException();\n\n                switch (_type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                    case AuthZoneType.Stub:\n                        return _apexZone.SyncFailed;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/CacheZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class CacheZone : Zone\n    {\n        #region variables\n\n        ConcurrentDictionary<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> _ecsEntries;\n\n        #endregion\n\n        #region constructor\n\n        public CacheZone(string name, int capacity)\n            : base(name, capacity)\n        { }\n\n        private CacheZone(string name, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entries)\n            : base(name, entries)\n        { }\n\n        #endregion\n\n        #region static\n\n        public static CacheZone ReadFrom(BinaryReader bR, bool serveStale)\n        {\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    string name = bR.ReadString();\n                    ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entries = ReadEntriesFrom(bR, serveStale);\n\n                    CacheZone cacheZone = new CacheZone(name, entries);\n\n                    //write all ECS cache records\n                    {\n                        int ecsCount = bR.ReadInt32();\n                        if (ecsCount > 0)\n                        {\n                            ConcurrentDictionary<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntries = new ConcurrentDictionary<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>>(1, ecsCount);\n\n                            for (int i = 0; i < ecsCount; i++)\n                            {\n                                NetworkAddress key = NetworkAddress.ReadFrom(bR);\n                                ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> ecsEntry = ReadEntriesFrom(bR, serveStale);\n\n                                if (!ecsEntry.IsEmpty)\n                                    ecsEntries.TryAdd(key, ecsEntry);\n                            }\n\n                            if (!ecsEntries.IsEmpty)\n                                cacheZone._ecsEntries = ecsEntries;\n                        }\n                    }\n\n                    return cacheZone;\n\n                default:\n                    throw new InvalidDataException(\"CacheZone format version not supported.\");\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private static IReadOnlyList<DnsResourceRecord> ValidateRRSet(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records, bool serveStale, bool skipSpecialCacheRecord)\n        {\n            foreach (DnsResourceRecord record in records)\n            {\n                if (record.IsExpired(serveStale))\n                    return Array.Empty<DnsResourceRecord>(); //RR Set is expired\n\n                if (skipSpecialCacheRecord && (record.RDATA is DnsCache.DnsSpecialCacheRecordData))\n                    return Array.Empty<DnsResourceRecord>(); //RR Set is special cache record\n            }\n\n            if (records.Count > 1)\n            {\n                switch (type)\n                {\n                    case DnsResourceRecordType.A:\n                    case DnsResourceRecordType.AAAA:\n                        List<DnsResourceRecord> newRecords = new List<DnsResourceRecord>(records);\n                        newRecords.Shuffle(); //shuffle records to allow load balancing\n                        return newRecords;\n                }\n            }\n\n            //update last used on\n            DateTime utcNow = DateTime.UtcNow;\n\n            foreach (DnsResourceRecord record in records)\n                record.GetCacheRecordInfo().LastUsedOn = utcNow;\n\n            return records;\n        }\n\n        private static ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> ReadEntriesFrom(BinaryReader bR, bool serveStale)\n        {\n            int count = bR.ReadInt32();\n            ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entries = new ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>(1, count);\n\n            for (int i = 0; i < count; i++)\n            {\n                DnsResourceRecordType key = (DnsResourceRecordType)bR.ReadUInt16();\n                int rrCount = bR.ReadInt32();\n                DnsResourceRecord[] records = new DnsResourceRecord[rrCount];\n\n                for (int j = 0; j < rrCount; j++)\n                {\n                    records[j] = DnsResourceRecord.ReadCacheRecordFrom(bR, delegate (DnsResourceRecord record)\n                    {\n                        record.Tag = new CacheRecordInfo(bR);\n                    });\n                }\n\n                if (!DnsResourceRecord.IsRRSetExpired(records, serveStale))\n                    entries.TryAdd(key, records);\n            }\n\n            return entries;\n        }\n\n        private static void WriteEntriesTo(ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entries, BinaryWriter bW)\n        {\n            bW.Write(entries.Count);\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in entries)\n            {\n                bW.Write((ushort)entry.Key);\n                bW.Write(entry.Value.Count);\n\n                foreach (DnsResourceRecord record in entry.Value)\n                {\n                    record.WriteCacheRecordTo(bW, delegate ()\n                    {\n                        if (record.Tag is not CacheRecordInfo rrInfo)\n                            rrInfo = CacheRecordInfo.Default; //default info\n\n                        rrInfo.WriteTo(bW);\n                    });\n                }\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public bool SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records, bool serveStale)\n        {\n            if (records.Count == 0)\n                return false;\n\n            ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entries;\n\n            CacheRecordInfo cacheRecordInfo = records[0].GetCacheRecordInfo();\n            NetworkAddress eDnsClientSubnet = cacheRecordInfo.EDnsClientSubnet;\n\n            if (eDnsClientSubnet is null)\n            {\n                entries = _entries;\n            }\n            else\n            {\n                if (_ecsEntries is null)\n                {\n                    _ecsEntries = new ConcurrentDictionary<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>>(1, 5);\n                    entries = new ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>(1, 1);\n                    if (!_ecsEntries.TryAdd(eDnsClientSubnet, entries))\n                        return false;\n                }\n                else if (!_ecsEntries.TryGetValue(eDnsClientSubnet, out entries))\n                {\n                    entries = new ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>(1, 1);\n                    if (!_ecsEntries.TryAdd(eDnsClientSubnet, entries))\n                        return false;\n                }\n            }\n\n            bool isFailureRecord = false;\n\n            if (records[0].RDATA is DnsCache.DnsSpecialCacheRecordData splRecord)\n            {\n                if (splRecord.IsFailureOrBadCache)\n                {\n                    //call trying to cache failure record\n                    isFailureRecord = true;\n\n                    if (entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords) && (existingRecords.Count > 0) && !DnsResourceRecord.IsRRSetExpired(existingRecords, serveStale))\n                    {\n                        if ((existingRecords[0].RDATA is not DnsCache.DnsSpecialCacheRecordData existingSplRecord) || !existingSplRecord.IsFailureOrBadCache)\n                            return false; //skip to avoid overwriting a useful record with a failure record\n\n                        //copy extended errors from existing spl record\n                        splRecord.CopyExtendedDnsErrorsFrom(existingSplRecord);\n                    }\n                }\n            }\n            else if (records[0].Type == DnsResourceRecordType.CHILD_NS)\n            {\n                //convert back RRSet to correct type\n                DnsResourceRecord[] newRecords = new DnsResourceRecord[records.Count];\n\n                for (int i = 0; i < records.Count; i++)\n                {\n                    DnsResourceRecord record = records[i];\n\n                    if (record.Type == DnsResourceRecordType.CHILD_NS)\n                        record = record.CloneAs(DnsResourceRecordType.NS);\n\n                    newRecords[i] = record;\n                }\n\n                records = newRecords;\n            }\n\n            //set last used date time\n            DateTime utcNow = DateTime.UtcNow;\n\n            foreach (DnsResourceRecord record in records)\n                record.GetCacheRecordInfo().LastUsedOn = utcNow;\n\n            //set records\n            bool added = true;\n\n            entries.AddOrUpdate(type, records, delegate (DnsResourceRecordType key, IReadOnlyList<DnsResourceRecord> existingRecords)\n            {\n                added = false;\n                return records;\n            });\n\n            if (serveStale && !isFailureRecord)\n            {\n                //remove stale CNAME entry only when serve stale is enabled\n                //making sure current record is not a failure record causing removal of useful stale CNAME record\n                switch (type)\n                {\n                    case DnsResourceRecordType.CNAME:\n                    case DnsResourceRecordType.SOA:\n                    case DnsResourceRecordType.NS:\n                    case DnsResourceRecordType.DS:\n                        //do nothing\n                        break;\n\n                    default:\n                        //remove stale CNAME entry since current new entry type overlaps any existing CNAME entry in cache\n                        //keeping both entries will create issue with serve stale implementation since stale CNAME entry will be always returned\n\n                        if (entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList<DnsResourceRecord> existingCNAMERecords))\n                        {\n                            if ((existingCNAMERecords.Count > 0) && (existingCNAMERecords[0].RDATA is DnsCNAMERecordData) && existingCNAMERecords[0].IsStale)\n                            {\n                                //delete CNAME entry only when it contains stale DnsCNAMERecord RDATA and not special cache records\n                                entries.TryRemove(DnsResourceRecordType.CNAME, out _);\n                            }\n                        }\n                        break;\n                }\n            }\n\n            return added;\n        }\n\n        public int RemoveExpiredRecords(bool serveStale)\n        {\n            int removedEntries = 0;\n\n            if (_ecsEntries is not null)\n            {\n                foreach (KeyValuePair<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntry in _ecsEntries)\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in ecsEntry.Value)\n                    {\n                        if (DnsResourceRecord.IsRRSetExpired(entry.Value, serveStale))\n                        {\n                            if (ecsEntry.Value.TryRemove(entry.Key, out _)) //RR Set is expired; remove entry\n                                removedEntries++;\n                        }\n                    }\n\n                    if (ecsEntry.Value.IsEmpty)\n                        _ecsEntries.TryRemove(ecsEntry.Key, out _);\n                }\n            }\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                if (DnsResourceRecord.IsRRSetExpired(entry.Value, serveStale))\n                {\n                    if (_entries.TryRemove(entry.Key, out _)) //RR Set is expired; remove entry\n                        removedEntries++;\n                }\n            }\n\n            return removedEntries;\n        }\n\n        public int RemoveLeastUsedRecords(DateTime cutoff)\n        {\n            int removedEntries = 0;\n\n            if (_ecsEntries is not null)\n            {\n                foreach (KeyValuePair<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntry in _ecsEntries)\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in ecsEntry.Value)\n                    {\n                        if ((entry.Value.Count == 0) || (entry.Value[0].GetCacheRecordInfo().LastUsedOn < cutoff))\n                        {\n                            if (ecsEntry.Value.TryRemove(entry.Key, out _)) //RR Set was last used before cutoff; remove entry\n                                removedEntries++;\n                        }\n                    }\n\n                    if (ecsEntry.Value.IsEmpty)\n                        _ecsEntries.TryRemove(ecsEntry.Key, out _);\n                }\n            }\n\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                if ((entry.Value.Count == 0) || (entry.Value[0].GetCacheRecordInfo().LastUsedOn < cutoff))\n                {\n                    if (_entries.TryRemove(entry.Key, out _)) //RR Set was last used before cutoff; remove entry\n                        removedEntries++;\n                }\n            }\n\n            return removedEntries;\n        }\n\n        public int DeleteEDnsClientSubnetData()\n        {\n            if (_ecsEntries is null)\n                return 0;\n\n            int count = 0;\n\n            foreach (KeyValuePair<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntry in _ecsEntries)\n                count += ecsEntry.Value.Count;\n\n            _ecsEntries = null;\n\n            return count;\n        }\n\n        public IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool serveStale, bool skipSpecialCacheRecord, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet)\n        {\n            ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entries;\n\n            if (eDnsClientSubnet is null)\n            {\n                entries = _entries;\n            }\n            else\n            {\n                if (_ecsEntries is null)\n                    return Array.Empty<DnsResourceRecord>();\n\n                if (advancedForwardingClientSubnet)\n                {\n                    if (!_ecsEntries.TryGetValue(eDnsClientSubnet, out entries))\n                        return Array.Empty<DnsResourceRecord>();\n                }\n                else\n                {\n                    NetworkAddress selectedNetwork = null;\n                    entries = null;\n\n                    foreach (KeyValuePair<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntry in _ecsEntries)\n                    {\n                        NetworkAddress cacheSubnet = ecsEntry.Key;\n\n                        if (cacheSubnet.PrefixLength > eDnsClientSubnet.PrefixLength)\n                            continue;\n\n                        if (cacheSubnet.Equals(eDnsClientSubnet) || cacheSubnet.Contains(eDnsClientSubnet.Address))\n                        {\n                            if ((selectedNetwork is null) || (cacheSubnet.PrefixLength < selectedNetwork.PrefixLength))\n                            {\n                                selectedNetwork = cacheSubnet;\n                                entries = ecsEntry.Value;\n                            }\n                        }\n                    }\n\n                    if (entries is null)\n                        return Array.Empty<DnsResourceRecord>();\n                }\n            }\n\n            switch (type)\n            {\n                case DnsResourceRecordType.DS:\n                    {\n                        //since some zones have CNAME at apex so no CNAME lookup for DS queries!\n                        if (entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n                            return ValidateRRSet(type, existingRecords, serveStale, skipSpecialCacheRecord);\n                    }\n                    break;\n\n                case DnsResourceRecordType.SOA:\n                case DnsResourceRecordType.DNSKEY:\n                    {\n                        //since some zones have CNAME at apex!\n                        if (entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n                            return ValidateRRSet(type, existingRecords, serveStale, skipSpecialCacheRecord);\n\n                        if (entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList<DnsResourceRecord> existingCNAMERecords))\n                        {\n                            IReadOnlyList<DnsResourceRecord> rrset = ValidateRRSet(type, existingCNAMERecords, serveStale, skipSpecialCacheRecord);\n                            if (rrset.Count > 0)\n                            {\n                                if ((type == DnsResourceRecordType.CNAME) || (rrset[0].RDATA is DnsCNAMERecordData))\n                                    return rrset;\n                            }\n                        }\n                    }\n                    break;\n\n                case DnsResourceRecordType.ANY:\n                    List<DnsResourceRecord> anyRecords = new List<DnsResourceRecord>(entries.Count * 2);\n\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in entries)\n                    {\n                        if (entry.Key == DnsResourceRecordType.DS)\n                            continue;\n\n                        anyRecords.AddRange(ValidateRRSet(type, entry.Value, serveStale, true));\n                    }\n\n                    return anyRecords;\n\n                default:\n                    {\n                        if (entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList<DnsResourceRecord> existingCNAMERecords))\n                        {\n                            IReadOnlyList<DnsResourceRecord> rrset = ValidateRRSet(type, existingCNAMERecords, serveStale, skipSpecialCacheRecord);\n                            if (rrset.Count > 0)\n                            {\n                                if ((type == DnsResourceRecordType.CNAME) || (rrset[0].RDATA is DnsCNAMERecordData))\n                                    return rrset;\n                            }\n                        }\n\n                        if (entries.TryGetValue(type, out IReadOnlyList<DnsResourceRecord> existingRecords))\n                            return ValidateRRSet(type, existingRecords, serveStale, skipSpecialCacheRecord);\n                    }\n                    break;\n            }\n\n            return Array.Empty<DnsResourceRecord>();\n        }\n\n        public override void ListAllRecords(List<DnsResourceRecord> records)\n        {\n            if (_ecsEntries is not null)\n            {\n                foreach (KeyValuePair<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntry in _ecsEntries)\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in ecsEntry.Value)\n                        records.AddRange(entry.Value);\n                }\n            }\n\n            base.ListAllRecords(records);\n        }\n\n        public override bool ContainsNameServerRecords()\n        {\n            if (!_entries.TryGetValue(DnsResourceRecordType.NS, out IReadOnlyList<DnsResourceRecord> records))\n            {\n                if ((_name.Length > 0) || !_entries.TryGetValue(DnsResourceRecordType.CHILD_NS, out records)) //root zone case\n                    return false;\n            }\n\n            foreach (DnsResourceRecord record in records)\n            {\n                if (record.IsStale)\n                    continue;\n\n                if (record.RDATA is DnsNSRecordData)\n                    return true;\n            }\n\n            return false;\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.Write((byte)1); //version\n\n            //cache zone info\n            bW.Write(_name);\n\n            //write all cache records\n            WriteEntriesTo(_entries, bW);\n\n            //write all ECS cache records\n            if (_ecsEntries is null)\n            {\n                bW.Write(0);\n            }\n            else\n            {\n                bW.Write(_ecsEntries.Count);\n\n                foreach (KeyValuePair<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntry in _ecsEntries)\n                {\n                    ecsEntry.Key.WriteTo(bW);\n                    WriteEntriesTo(ecsEntry.Value, bW);\n                }\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool IsEmpty\n        {\n            get\n            {\n                if (_ecsEntries is null)\n                    return _entries.IsEmpty;\n\n                return _ecsEntries.IsEmpty && _entries.IsEmpty;\n            }\n        }\n\n        public int TotalEntries\n        {\n            get\n            {\n                if (_ecsEntries is null)\n                    return _entries.Count;\n\n                int count = _entries.Count;\n\n                foreach (KeyValuePair<NetworkAddress, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>> ecsEntry in _ecsEntries)\n                    count += ecsEntry.Value.Count;\n\n                return count;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/CatalogSubDomainZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class CatalogSubDomainZone : ForwarderSubDomainZone\n    {\n        #region constructor\n\n        public CatalogSubDomainZone(CatalogZone catalogZone, string name)\n            : base(catalogZone, name)\n        { }\n\n        #endregion\n\n        #region public\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            return []; //catalog zone is not queriable\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/CatalogZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Threading;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class CatalogZone : ForwarderZone\n    {\n        #region variables\n\n        readonly Dictionary<string, string> _membersIndex = new Dictionary<string, string>();\n        readonly ReaderWriterLockSlim _membersIndexLock = new ReaderWriterLockSlim();\n\n        #endregion\n\n        #region constructor\n\n        public CatalogZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(dnsServer, zoneInfo)\n        { }\n\n        public CatalogZone(DnsServer dnsServer, string name)\n            : base(dnsServer, name)\n        { }\n\n        #endregion\n\n        #region IDisposable\n\n        protected override void Dispose(bool disposing)\n        {\n            try\n            {\n                _membersIndexLock.Dispose();\n            }\n            finally\n            {\n                base.Dispose(disposing);\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        internal override void InitZone()\n        {\n            //init catalog zone with dummy SOA and NS records\n            DnsSOARecordData soa = new DnsSOARecordData(\"invalid\", \"invalid\", 1, 300, 60, 604800, 900);\n            DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);\n            soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n            _entries[DnsResourceRecordType.SOA] = [soaRecord];\n            _entries[DnsResourceRecordType.NS] = [new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, 0, new DnsNSRecordData(\"invalid\"))];\n        }\n\n        internal void InitZoneProperties()\n        {\n            //set catalog zone version record\n            _dnsServer.AuthZoneManager.SetRecord(_name, new DnsResourceRecord(\"version.\" + _name, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(\"2\")));\n\n            //init catalog global properties\n            QueryAccess = AuthZoneQueryAccess.Allow;\n            ZoneTransfer = AuthZoneTransfer.Deny;\n        }\n\n        internal void BuildMembersIndex()\n        {\n            foreach (KeyValuePair<string, string> memberEntry in EnumerateCatalogMemberZones(_dnsServer))\n                _membersIndex.TryAdd(memberEntry.Key.ToLowerInvariant(), memberEntry.Value);\n        }\n\n        #endregion\n\n        #region catalog\n\n        public void AddMemberZone(string memberZoneName, AuthZoneType zoneType)\n        {\n            memberZoneName = memberZoneName.ToLowerInvariant();\n\n            _membersIndexLock.EnterWriteLock();\n            try\n            {\n                if (_membersIndex.TryGetValue(memberZoneName, out _))\n                {\n                    if (_membersIndex.Remove(memberZoneName, out string removedMemberZoneDomain))\n                    {\n                        foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, removedMemberZoneDomain, true))\n                            _dnsServer.AuthZoneManager.DeleteRecord(_name, record);\n                    }\n                }\n\n                string memberZoneDomain = GetDomainWithLabel(\"zones.\" + _name);\n                DateTime utcNow = DateTime.UtcNow;\n\n                DnsResourceRecord ptrRecord = new DnsResourceRecord(memberZoneDomain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(memberZoneName));\n                ptrRecord.GetAuthGenericRecordInfo().LastModified = utcNow;\n\n                DnsResourceRecord txtRecord = new DnsResourceRecord(\"zone-type.ext.\" + memberZoneDomain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(zoneType.ToString().ToLowerInvariant()));\n                txtRecord.GetAuthGenericRecordInfo().LastModified = utcNow;\n\n                _dnsServer.AuthZoneManager.AddRecord(_name, ptrRecord);\n                _dnsServer.AuthZoneManager.AddRecord(_name, txtRecord);\n\n                _membersIndex[memberZoneName] = memberZoneDomain;\n            }\n            finally\n            {\n                _membersIndexLock.ExitWriteLock();\n            }\n        }\n\n        public bool RemoveMemberZone(string memberZoneName)\n        {\n            memberZoneName = memberZoneName.ToLowerInvariant();\n\n            _membersIndexLock.EnterWriteLock();\n            try\n            {\n                if (_membersIndex.Remove(memberZoneName, out string removedMemberZoneDomain))\n                {\n                    foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, removedMemberZoneDomain, true))\n                        _dnsServer.AuthZoneManager.DeleteRecord(_name, record);\n\n                    return true;\n                }\n\n                return false;\n            }\n            finally\n            {\n                _membersIndexLock.ExitWriteLock();\n            }\n        }\n\n        public void ChangeMemberZoneOwnership(string memberZoneName, string newCatalogZoneName)\n        {\n            string memberZoneDomain = GetMemberZoneDomain(memberZoneName);\n            string domain = \"coo.\" + memberZoneDomain;\n\n            DateTime utcNow = DateTime.UtcNow;\n            uint soaExpiry = GetZoneSoaExpire();\n\n            //add COO record with expiry\n            DnsResourceRecord cooRecord = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(newCatalogZoneName));\n            GenericRecordInfo cooRecordInfo = cooRecord.GetAuthGenericRecordInfo();\n            cooRecordInfo.LastModified = utcNow;\n            cooRecordInfo.ExpiryTtl = soaExpiry;\n\n            _dnsServer.AuthZoneManager.SetRecord(_name, cooRecord);\n\n            //set expiry for other member zone records\n            foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, memberZoneDomain, true))\n            {\n                GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();\n                recordInfo.LastModified = utcNow;\n                recordInfo.ExpiryTtl = soaExpiry;\n            }\n        }\n\n        public IReadOnlyCollection<string> GetAllMemberZoneNames()\n        {\n            _membersIndexLock.EnterReadLock();\n            try\n            {\n                return _membersIndex.Keys.ToArray();\n            }\n            finally\n            {\n                _membersIndexLock.ExitReadLock();\n            }\n        }\n\n        public AuthZoneType GetZoneTypeProperty(string memberZoneName)\n        {\n            string domain = \"zone-type.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT);\n            if (records.Count > 0)\n                return Enum.Parse<AuthZoneType>((records[0].RDATA as DnsTXTRecordData).GetText(), true);\n\n            return AuthZoneType.Primary;\n        }\n\n        public void SetAllowQueryProperty(IReadOnlyCollection<NetworkAccessControl> acl = null, string memberZoneName = null)\n        {\n            string domain = \"allow-query.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            if (acl is null)\n            {\n                _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.APL);\n            }\n            else\n            {\n                DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.APL, DnsClass.IN, 0, NetworkAccessControl.ConvertToAPLRecordData(acl));\n                record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                _dnsServer.AuthZoneManager.SetRecord(_name, record);\n            }\n        }\n\n        public void SetAllowTransferProperty(IReadOnlyCollection<NetworkAccessControl> acl = null, string memberZoneName = null)\n        {\n            string domain = \"allow-transfer.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            if (acl is null)\n            {\n                _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.APL);\n            }\n            else\n            {\n                DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.APL, DnsClass.IN, 0, NetworkAccessControl.ConvertToAPLRecordData(acl));\n                record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                _dnsServer.AuthZoneManager.SetRecord(_name, record);\n            }\n        }\n\n        public void SetZoneTransferTsigKeyNamesProperty(IReadOnlySet<string> tsigKeyNames = null, string memberZoneName = null)\n        {\n            string domain = \"transfer-tsig-key-names.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            if (tsigKeyNames is null)\n            {\n                _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.PTR);\n            }\n            else\n            {\n                DnsResourceRecord[] records = new DnsResourceRecord[tsigKeyNames.Count];\n                int i = 0;\n\n                foreach (string entry in tsigKeyNames)\n                {\n                    DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(entry));\n                    record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                    records[i++] = record;\n                }\n\n                _dnsServer.AuthZoneManager.SetRecords(_name, records);\n            }\n        }\n\n        public void SetPrimaryAddressesProperty(IReadOnlyList<NameServerAddress> primaryServerAddresses = null, string memberZoneName = null)\n        {\n            string domain = \"primary-addresses.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            if (primaryServerAddresses is null)\n            {\n                _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.TXT);\n            }\n            else\n            {\n                IReadOnlyList<string> charStrings = primaryServerAddresses.Convert(delegate (NameServerAddress nameServer)\n                {\n                    return nameServer.ToString();\n                });\n\n                DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(charStrings));\n                record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                _dnsServer.AuthZoneManager.SetRecord(_name, record);\n            }\n        }\n\n        public void SetPrimaryZoneTransferProtocolProperty(DnsTransportProtocol? zoneTransferProtocol = null, string memberZoneName = null)\n        {\n            string domain = \"primary-transfer-protocol.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            if (zoneTransferProtocol is null)\n            {\n                _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.TXT);\n            }\n            else\n            {\n                DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(zoneTransferProtocol.ToString()));\n                record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                _dnsServer.AuthZoneManager.SetRecord(_name, record);\n            }\n        }\n\n        public void SetPrimaryZoneTransferTsigKeyNameProperty(string tsigKeyName = null, string memberZoneName = null)\n        {\n            string domain = \"primary-transfer-tsig-key-name.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            if (tsigKeyName is null)\n            {\n                _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.PTR);\n            }\n            else\n            {\n                DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(tsigKeyName));\n                record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                _dnsServer.AuthZoneManager.SetRecord(_name, record);\n            }\n        }\n\n        public void SetZoneMdValidationProperty(bool? validateZone = null, string memberZoneName = null)\n        {\n            string domain = \"zonemd-validation.ext.\" + GetMemberZoneDomain(memberZoneName);\n\n            if (validateZone is null)\n            {\n                _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.TXT);\n            }\n            else\n            {\n                DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(validateZone.ToString()));\n                record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                _dnsServer.AuthZoneManager.SetRecord(_name, record);\n            }\n        }\n\n        private string GetMemberZoneDomain(string memberZoneName = null)\n        {\n            if (memberZoneName is null)\n            {\n                return _name;\n            }\n            else\n            {\n                memberZoneName = memberZoneName.ToLowerInvariant();\n\n                _membersIndexLock.EnterReadLock();\n                try\n                {\n                    if (!_membersIndex.TryGetValue(memberZoneName, out string memberZoneDomain))\n                        throw new DnsServerException(\"Failed to find '\" + memberZoneName + \"' member zone entry in '\" + ToString() + \"' Catalog zone: member zone does not exists.\");\n\n                    return memberZoneDomain;\n                }\n                finally\n                {\n                    _membersIndexLock.ExitReadLock();\n                }\n            }\n        }\n\n        private string GetDomainWithLabel(string domain)\n        {\n            Span<byte> buffer = stackalloc byte[8];\n            int i = 0;\n\n            do\n            {\n                RandomNumberGenerator.Fill(buffer);\n                string label = Base32.ToBase32HexString(buffer, true).ToLowerInvariant();\n                string domainWithLabel = label + \".\" + domain;\n\n                if (_dnsServer.AuthZoneManager.NameExists(_name, domainWithLabel))\n                    continue;\n\n                return domainWithLabel;\n            }\n            while (++i < 10);\n\n            throw new DnsServerException(\"Failed to generate unique label for the given domain name '\" + domain + \"'. Please try again.\");\n        }\n\n        #endregion\n\n        #region public\n\n        public override string GetZoneTypeName()\n        {\n            return \"Catalog\";\n        }\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.SOA:\n                    if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase))\n                        throw new InvalidOperationException(\"Invalid SOA record.\");\n\n                    DnsResourceRecord newSoaRecord = records[0];\n                    DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData;\n\n                    //reset fixed record values\n                    DnsSOARecordData modifiedSoa = new DnsSOARecordData(\"invalid\", \"invalid\", newSoa.Serial, newSoa.Refresh, newSoa.Retry, newSoa.Expire, newSoa.Minimum);\n                    DnsResourceRecord modifiedSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, modifiedSoa) { Tag = newSoaRecord.Tag };\n\n                    base.SetRecords(type, [modifiedSoaRecord]);\n                    break;\n\n                default:\n                    throw new InvalidOperationException(\"Cannot set records in Catalog zone.\");\n            }\n\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            throw new InvalidOperationException(\"Cannot add record in Catalog zone.\");\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            throw new InvalidOperationException(\"Cannot delete record in Catalog zone.\");\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)\n        {\n            throw new InvalidOperationException(\"Cannot delete records in Catalog zone.\");\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            throw new InvalidOperationException(\"Cannot update record in Catalog zone.\");\n        }\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            if (type == DnsResourceRecordType.SOA)\n                return base.QueryRecords(type, dnssecOk); //allow SOA for zone transfer to work with bind\n\n            return []; //catalog zone is not queriable\n        }\n\n        #endregion\n\n        #region properties\n\n        public override string CatalogZoneName\n        {\n            get { return base.CatalogZoneName; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override bool OverrideCatalogQueryAccess\n        {\n            get { return base.OverrideCatalogQueryAccess; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override bool OverrideCatalogZoneTransfer\n        {\n            get { return base.OverrideCatalogZoneTransfer; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override bool OverrideCatalogNotify\n        {\n            get { return base.OverrideCatalogNotify; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneUpdate Update\n        {\n            get { return base.Update; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/ForwarderSubDomainZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class ForwarderSubDomainZone : SubDomainZone\n    {\n        #region variables\n\n        readonly ForwarderZone _forwarderZone;\n\n        #endregion\n\n        #region constructor\n\n        public ForwarderSubDomainZone(ForwarderZone forwarderZone, string name)\n            : base(forwarderZone, name)\n        {\n            _forwarderZone = forwarderZone;\n        }\n\n        #endregion\n\n        #region public\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot set SOA record on sub domain.\");\n\n                case DnsResourceRecordType.DS:\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot set DNSSEC records.\");\n\n                default:\n                    if (records[0].OriginalTtlValue > _forwarderZone.GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot set records: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TrySetRecords(type, records, out IReadOnlyList<DnsResourceRecord> deletedRecords))\n                        throw new DnsServerException(\"Cannot set records. Please try again.\");\n\n                    _forwarderZone.CommitAndIncrementSerial(deletedRecords, records);\n\n                    _forwarderZone.TriggerNotify();\n                    break;\n            }\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            switch (record.Type)\n            {\n                case DnsResourceRecordType.DS:\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot add DNSSEC record.\");\n\n                default:\n                    if (record.OriginalTtlValue > _forwarderZone.GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot add record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    AddRecord(record, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    if (addedRecords.Count > 0)\n                    {\n                        _forwarderZone.CommitAndIncrementSerial(deletedRecords, addedRecords);\n\n                        _forwarderZone.TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            if (_entries.TryRemove(type, out IReadOnlyList<DnsResourceRecord> removedRecords))\n            {\n                _forwarderZone.CommitAndIncrementSerial(removedRecords);\n\n                _forwarderZone.TriggerNotify();\n\n                return true;\n            }\n\n            return false;\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata)\n        {\n            if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord))\n            {\n                _forwarderZone.CommitAndIncrementSerial([deletedRecord]);\n\n                _forwarderZone.TriggerNotify();\n\n                return true;\n            }\n\n            return false;\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            switch (oldRecord.Type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot update record: use SetRecords() for \" + oldRecord.Type.ToString() + \" record.\");\n\n                default:\n                    if (oldRecord.Type != newRecord.Type)\n                        throw new InvalidOperationException(\"Old and new record types do not match.\");\n\n                    if (newRecord.OriginalTtlValue > _forwarderZone.GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot update record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord))\n                        throw new InvalidOperationException(\"Cannot update record: the record does not exists to be updated.\");\n\n                    AddRecord(newRecord, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    List<DnsResourceRecord> allDeletedRecords = new List<DnsResourceRecord>(deletedRecords.Count + 1);\n                    allDeletedRecords.Add(deletedRecord);\n                    allDeletedRecords.AddRange(deletedRecords);\n\n                    _forwarderZone.CommitAndIncrementSerial(allDeletedRecords, addedRecords);\n\n                    _forwarderZone.TriggerNotify();\n                    break;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/ForwarderZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class ForwarderZone : ApexZone\n    {\n        #region constructor\n\n        public ForwarderZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(dnsServer, zoneInfo)\n        {\n            InitNotify();\n            InitRecordExpiry();\n        }\n\n        public ForwarderZone(DnsServer dnsServer, string name)\n            : base(dnsServer, name)\n        {\n            InitZone();\n            InitNotify();\n            InitRecordExpiry();\n        }\n\n        public ForwarderZone(DnsServer dnsServer, string name, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, DnsForwarderRecordProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments)\n            : base(dnsServer, name)\n        {\n            DnsResourceRecord fwdRecord = new DnsResourceRecord(name, DnsResourceRecordType.FWD, DnsClass.IN, 0, new DnsForwarderRecordData(forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, 0));\n\n            if (!string.IsNullOrEmpty(fwdRecordComments))\n                fwdRecord.GetAuthGenericRecordInfo().Comments = fwdRecordComments;\n\n            fwdRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n            _entries[DnsResourceRecordType.FWD] = [fwdRecord];\n\n            InitZone();\n            InitNotify();\n            InitRecordExpiry();\n        }\n\n        #endregion\n\n        #region internal\n\n        internal virtual void InitZone()\n        {\n            //init forwarder zone with dummy SOA record\n            DnsSOARecordData soa = new DnsSOARecordData(_dnsServer.ServerDomain, \"invalid\", 1, 900, 300, 604800, 900);\n            DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);\n            soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n            _entries[DnsResourceRecordType.SOA] = [soaRecord];\n        }\n\n        #endregion\n\n        #region public\n\n        public override string GetZoneTypeName()\n        {\n            return \"Conditional Forwarder\";\n        }\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.CNAME:\n                    throw new InvalidOperationException(\"Cannot set CNAME record at zone apex.\");\n\n                case DnsResourceRecordType.SOA:\n                    if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase))\n                        throw new InvalidOperationException(\"Invalid SOA record.\");\n\n                    DnsResourceRecord newSoaRecord = records[0];\n                    DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData;\n\n                    if (newSoaRecord.OriginalTtlValue > newSoa.Expire)\n                        throw new DnsServerException(\"Cannot set record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (newSoa.Retry > newSoa.Refresh)\n                        throw new DnsServerException(\"Cannot set record: SOA RETRY cannot be greater than SOA REFRESH.\");\n\n                    if (newSoa.Refresh > newSoa.Expire)\n                        throw new DnsServerException(\"Cannot set record: SOA REFRESH cannot be greater than SOA EXPIRE.\");\n\n                    {\n                        //reset fixed record values\n                        DnsSOARecordData modifiedSoa = new DnsSOARecordData(newSoa.PrimaryNameServer, \"invalid\", newSoa.Serial, newSoa.Refresh, newSoa.Retry, newSoa.Expire, newSoa.Minimum);\n                        newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, modifiedSoa) { Tag = newSoaRecord.Tag };\n                        records = [newSoaRecord];\n                    }\n\n                    //remove any record info except serial date scheme and comments\n                    bool useSoaSerialDateScheme;\n                    string comments;\n                    {\n                        SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo();\n\n                        useSoaSerialDateScheme = recordInfo.UseSoaSerialDateScheme;\n                        comments = recordInfo.Comments;\n                    }\n\n                    newSoaRecord.Tag = null; //remove old record info\n\n                    {\n                        SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo();\n\n                        recordInfo.UseSoaSerialDateScheme = useSoaSerialDateScheme;\n                        recordInfo.Comments = comments;\n                        recordInfo.LastModified = DateTime.UtcNow;\n                    }\n\n                    //setting new SOA\n                    CommitAndIncrementSerial(null, records);\n\n                    TriggerNotify();\n                    break;\n\n                case DnsResourceRecordType.DS:\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot set DNSSEC records.\");\n\n                default:\n                    if (records[0].OriginalTtlValue > GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot set records: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TrySetRecords(type, records, out IReadOnlyList<DnsResourceRecord> deletedRecords))\n                        throw new DnsServerException(\"Cannot set records. Please try again.\");\n\n                    CommitAndIncrementSerial(deletedRecords, records);\n\n                    TriggerNotify();\n                    break;\n            }\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            switch (record.Type)\n            {\n                case DnsResourceRecordType.DS:\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot set DNSSEC records.\");\n\n                default:\n                    if (record.OriginalTtlValue > GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot add record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    AddRecord(record, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    if (addedRecords.Count > 0)\n                    {\n                        CommitAndIncrementSerial(deletedRecords, addedRecords);\n\n                        TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot delete SOA record.\");\n\n                default:\n                    if (_entries.TryRemove(type, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                    {\n                        CommitAndIncrementSerial(removedRecords);\n\n                        TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot delete SOA record.\");\n\n                default:\n                    if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord))\n                    {\n                        CommitAndIncrementSerial([deletedRecord]);\n\n                        TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            switch (oldRecord.Type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot update record: use SetRecords() for \" + oldRecord.Type.ToString() + \" record\");\n\n                default:\n                    if (oldRecord.Type != newRecord.Type)\n                        throw new InvalidOperationException(\"Old and new record types do not match.\");\n\n                    if (newRecord.OriginalTtlValue > GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot update record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord))\n                        throw new DnsServerException(\"Cannot update record: the record does not exists to be updated.\");\n\n                    AddRecord(newRecord, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    List<DnsResourceRecord> allDeletedRecords = new List<DnsResourceRecord>(deletedRecords.Count + 1);\n                    allDeletedRecords.Add(deletedRecord);\n                    allDeletedRecords.AddRange(deletedRecords);\n\n                    CommitAndIncrementSerial(allDeletedRecords, addedRecords);\n\n                    TriggerNotify();\n                    break;\n            }\n        }\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            if (this is CatalogZone)\n                return base.QueryRecords(type, dnssecOk);\n\n            if (type == DnsResourceRecordType.SOA)\n                return []; //forwarder zone is not authoritative and contains dummy SOA record\n\n            return base.QueryRecords(type, dnssecOk);\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool Disabled\n        {\n            get { return base.Disabled; }\n            set\n            {\n                if (base.Disabled == value)\n                    return;\n\n                base.Disabled = value; //set value early to be able to use it for notify\n\n                if (value)\n                    DisableNotifyTimer();\n                else\n                    TriggerNotify();\n            }\n        }\n\n        public override AuthZoneQueryAccess QueryAccess\n        {\n            get { return base.QueryAccess; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneQueryAccess.AllowOnlyZoneNameServers:\n                    case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        throw new ArgumentException(\"The Query Access option is invalid for \" + GetZoneTypeName() + \" zones: \" + value.ToString(), nameof(QueryAccess));\n                }\n\n                base.QueryAccess = value;\n            }\n        }\n\n        public override AuthZoneTransfer ZoneTransfer\n        {\n            get { return base.ZoneTransfer; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneTransfer.AllowOnlyZoneNameServers:\n                    case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        throw new ArgumentException(\"The Zone Transfer option is invalid for \" + GetZoneTypeName() + \" zones: \" + value.ToString(), nameof(ZoneTransfer));\n                }\n\n                base.ZoneTransfer = value;\n            }\n        }\n\n        public override AuthZoneNotify Notify\n        {\n            get { return base.Notify; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneNotify.ZoneNameServers:\n                    case AuthZoneNotify.BothZoneAndSpecifiedNameServers:\n                        throw new ArgumentException(\"The Notify option is invalid for \" + GetZoneTypeName() + \" zones: \" + value.ToString(), nameof(Notify));\n\n                    case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones:\n                        if (this is CatalogZone)\n                            break;\n\n                        throw new ArgumentException(\"The Notify option is invalid for \" + GetZoneTypeName() + \" zones: \" + value.ToString(), nameof(Notify));\n                }\n\n                base.Notify = value;\n            }\n        }\n\n        public override AuthZoneUpdate Update\n        {\n            get { return base.Update; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneUpdate.AllowOnlyZoneNameServers:\n                    case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        throw new ArgumentException(\"The Dynamic Updates option is invalid for \" + GetZoneTypeName() + \" zones: \" + value.ToString(), nameof(Update));\n                }\n\n                base.Update = value;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/PrimarySubDomainZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class PrimarySubDomainZone : SubDomainZone\n    {\n        #region variables\n\n        readonly PrimaryZone _primaryZone;\n\n        #endregion\n\n        #region constructor\n\n        public PrimarySubDomainZone(PrimaryZone primaryZone, string name)\n            : base(primaryZone, name)\n        {\n            _primaryZone = primaryZone;\n        }\n\n        #endregion\n\n        #region DNSSEC\n\n        internal override IReadOnlyList<DnsResourceRecord> SignRRSet(IReadOnlyList<DnsResourceRecord> records)\n        {\n            return _primaryZone.SignRRSet(records);\n        }\n\n        #endregion\n\n        #region public\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n            {\n                switch (type)\n                {\n                    case DnsResourceRecordType.ANAME:\n                    case DnsResourceRecordType.APP:\n                        throw new DnsServerException(\"The record type is not supported by DNSSEC signed primary zones.\");\n\n                    default:\n                        foreach (DnsResourceRecord record in records)\n                        {\n                            if (record.GetAuthGenericRecordInfo().Disabled)\n                                throw new DnsServerException(\"Cannot set records: disabling records in a signed zones is not supported.\");\n                        }\n\n                        break;\n                }\n            }\n\n            switch (type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot set SOA record on sub domain.\");\n\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot set DNSSEC records.\");\n\n                case DnsResourceRecordType.FWD:\n                    throw new DnsServerException(\"The record type is not supported by primary zones.\");\n\n                default:\n                    if (records[0].OriginalTtlValue > _primaryZone.GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot set records: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TrySetRecords(type, records, out IReadOnlyList<DnsResourceRecord> deletedRecords))\n                        throw new DnsServerException(\"Cannot set records. Please try again.\");\n\n                    _primaryZone.CommitAndIncrementSerial(deletedRecords, records);\n\n                    if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                        _primaryZone.UpdateDnssecRecordsFor(this, type);\n\n                    _primaryZone.TriggerNotify();\n                    break;\n            }\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n            {\n                switch (record.Type)\n                {\n                    case DnsResourceRecordType.ANAME:\n                    case DnsResourceRecordType.APP:\n                        throw new DnsServerException(\"The record type is not supported by DNSSEC signed primary zones.\");\n\n                    default:\n                        if (record.GetAuthGenericRecordInfo().Disabled)\n                            throw new DnsServerException(\"Cannot add record: disabling records in a signed zones is not supported.\");\n\n                        break;\n                }\n            }\n\n            switch (record.Type)\n            {\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot add DNSSEC record.\");\n\n                case DnsResourceRecordType.FWD:\n                    throw new DnsServerException(\"The record type is not supported by primary zones.\");\n\n                default:\n                    if (record.OriginalTtlValue > _primaryZone.GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot add record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    AddRecord(record, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    if (addedRecords.Count > 0)\n                    {\n                        _primaryZone.CommitAndIncrementSerial(deletedRecords, addedRecords);\n\n                        if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                            _primaryZone.UpdateDnssecRecordsFor(this, record.Type);\n\n                        _primaryZone.TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot delete DNSSEC records.\");\n\n                default:\n                    if (_entries.TryRemove(type, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                    {\n                        _primaryZone.CommitAndIncrementSerial(removedRecords);\n\n                        if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                            _primaryZone.UpdateDnssecRecordsFor(this, type);\n\n                        _primaryZone.TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot delete DNSSEC records.\");\n\n                default:\n                    if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord))\n                    {\n                        _primaryZone.CommitAndIncrementSerial([deletedRecord]);\n\n                        if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                            _primaryZone.UpdateDnssecRecordsFor(this, type);\n\n                        _primaryZone.TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            switch (oldRecord.Type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot update record: use SetRecords() for \" + oldRecord.Type.ToString() + \" record.\");\n\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot update DNSSEC records.\");\n\n                default:\n                    if (oldRecord.Type != newRecord.Type)\n                        throw new InvalidOperationException(\"Old and new record types do not match.\");\n\n                    if ((_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.GetAuthGenericRecordInfo().Disabled)\n                        throw new DnsServerException(\"Cannot update record: disabling records in a signed zones is not supported.\");\n\n                    if (newRecord.OriginalTtlValue > _primaryZone.GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot update record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord))\n                        throw new InvalidOperationException(\"Cannot update record: the record does not exists to be updated.\");\n\n                    AddRecord(newRecord, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    List<DnsResourceRecord> allDeletedRecords = new List<DnsResourceRecord>(deletedRecords.Count + 1);\n                    allDeletedRecords.Add(deletedRecord);\n                    allDeletedRecords.AddRange(deletedRecords);\n\n                    _primaryZone.CommitAndIncrementSerial(allDeletedRecords, addedRecords);\n\n                    if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                        _primaryZone.UpdateDnssecRecordsFor(this, oldRecord.Type);\n\n                    _primaryZone.TriggerNotify();\n                    break;\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/PrimaryZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Dnssec;\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.ZoneManagers;\nusing System;\nusing System.Collections.Generic;\nusing System.Security.Cryptography;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    public enum AuthZoneDnssecStatus : byte\n    {\n        Unsigned = 0,\n        SignedWithNSEC = 1,\n        SignedWithNSEC3 = 2,\n    }\n\n    //DNSSEC Operational Practices, Version 2\n    //https://datatracker.ietf.org/doc/html/rfc6781\n\n    //DNSSEC Key Rollover Timing Considerations\n    //https://datatracker.ietf.org/doc/html/rfc7583\n\n    class PrimaryZone : ApexZone\n    {\n        #region variables\n\n        readonly bool _internal;\n\n        Dictionary<ushort, DnssecPrivateKey> _dnssecPrivateKeys;\n        const uint DNSSEC_SIGNATURE_INCEPTION_OFFSET = 60 * 60;\n\n        Timer _dnssecTimer;\n        const int DNSSEC_TIMER_INITIAL_INTERVAL = 30000;\n        internal const int DNSSEC_TIMER_PERIODIC_INTERVAL = 900000;\n\n        DateTime _lastSignatureRefreshCheckedOn;\n        readonly object _dnssecUpdateLock = new object();\n\n        #endregion\n\n        #region constructor\n\n        public PrimaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(dnsServer, zoneInfo)\n        {\n            IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys;\n            if (dnssecPrivateKeys is not null)\n            {\n                _dnssecPrivateKeys = new Dictionary<ushort, DnssecPrivateKey>(dnssecPrivateKeys.Count);\n\n                foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)\n                    _dnssecPrivateKeys.Add(dnssecPrivateKey.KeyTag, dnssecPrivateKey);\n            }\n\n            InitNotify();\n            InitRecordExpiry();\n        }\n\n        public PrimaryZone(DnsServer dnsServer, string name, bool @internal, bool useSoaSerialDateScheme)\n            : base(dnsServer, name)\n        {\n            _internal = @internal;\n\n            if (!_internal)\n            {\n                InitNotify();\n                InitRecordExpiry();\n\n                ZoneTransfer = AuthZoneTransfer.AllowOnlyZoneNameServers;\n                Notify = AuthZoneNotify.ZoneNameServers;\n            }\n\n            string rp;\n\n            if (_dnsServer.DefaultResponsiblePerson is null)\n                rp = _name.Length == 0 ? _dnsServer.ResponsiblePerson.Address : \"hostadmin@\" + _name;\n            else\n                rp = _dnsServer.DefaultResponsiblePerson.Address;\n\n            uint serial = GetNewSerial(0, 0, useSoaSerialDateScheme);\n            DnsSOARecordData soa = new DnsSOARecordData(_dnsServer.ServerDomain, rp, serial, 900, 300, 604800, dnsServer.AuthZoneManager.DefaultSoaRecordTtl);\n            DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Minimum, soa);\n            soaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme = useSoaSerialDateScheme;\n            soaRecord.GetAuthSOARecordInfo().LastModified = DateTime.UtcNow;\n\n            DnsResourceRecord nsRecord = new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, dnsServer.AuthZoneManager.DefaultNsRecordTtl, new DnsNSRecordData(soa.PrimaryNameServer));\n            nsRecord.GetAuthNSRecordInfo().LastModified = DateTime.UtcNow;\n\n            _entries[DnsResourceRecordType.SOA] = [soaRecord];\n            _entries[DnsResourceRecordType.NS] = [nsRecord];\n        }\n\n        internal PrimaryZone(DnsServer dnsServer, string name, DnsSOARecordData soa, DnsNSRecordData ns)\n            : base(dnsServer, name)\n        {\n            _internal = true;\n\n            _entries[DnsResourceRecordType.SOA] = [new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Minimum, soa)];\n            _entries[DnsResourceRecordType.NS] = [new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, dnsServer.AuthZoneManager.DefaultNsRecordTtl, ns)];\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected override void Dispose(bool disposing)\n        {\n            try\n            {\n                if (_disposed)\n                    return;\n\n                if (disposing)\n                {\n                    Timer dnssecTimer = _dnssecTimer;\n                    if (dnssecTimer is not null)\n                    {\n                        lock (dnssecTimer)\n                        {\n                            dnssecTimer.Dispose();\n                            _dnssecTimer = null;\n                        }\n                    }\n                }\n\n                _disposed = true;\n            }\n            finally\n            {\n                base.Dispose(disposing);\n            }\n        }\n\n        #endregion\n\n        #region DNSSEC\n\n        internal override void UpdateDnssecStatus()\n        {\n            base.UpdateDnssecStatus();\n\n            if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n            {\n                if (_dnssecPrivateKeys is not null)\n                    _dnssecTimer = new Timer(DnssecTimerCallback, null, DNSSEC_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private async void DnssecTimerCallback(object state)\n        {\n            try\n            {\n                List<DnssecPrivateKey> kskToReady = null;\n                List<DnssecPrivateKey> kskToActivate = null;\n                List<DnssecPrivateKey> kskToRetire = null;\n                List<DnssecPrivateKey> kskToRevoke = null;\n\n                List<DnssecPrivateKey> zskToActivate = null;\n                List<DnssecPrivateKey> zskToRetire = null;\n                List<DnssecPrivateKey> zskToRollover = null;\n\n                List<DnssecPrivateKey> keysToUnpublish = null;\n\n                bool saveZone = false;\n\n                lock (_dnssecPrivateKeys)\n                {\n                    foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                    {\n                        DnssecPrivateKey privateKey = privateKeyEntry.Value;\n\n                        if (privateKey.KeyType == DnssecPrivateKeyType.KeySigningKey)\n                        {\n                            //KSK\n                            switch (privateKey.State)\n                            {\n                                case DnssecPrivateKeyState.Published:\n                                    if (DateTime.UtcNow > privateKey.StateTransitionBy)\n                                    {\n                                        //long enough time for old RRsets to expire from caches\n                                        if (kskToReady is null)\n                                            kskToReady = new List<DnssecPrivateKey>();\n\n                                        kskToReady.Add(privateKey);\n                                    }\n                                    break;\n\n                                case DnssecPrivateKeyState.Ready:\n                                    if (privateKey.IsRetiring)\n                                    {\n                                        if (kskToRetire is null)\n                                            kskToRetire = new List<DnssecPrivateKey>();\n\n                                        kskToRetire.Add(privateKey);\n                                    }\n                                    else\n                                    {\n                                        if (kskToActivate is null)\n                                            kskToActivate = new List<DnssecPrivateKey>();\n\n                                        kskToActivate.Add(privateKey);\n                                    }\n                                    break;\n\n                                case DnssecPrivateKeyState.Active:\n                                    if (privateKey.IsRetiring)\n                                    {\n                                        if (kskToRetire is null)\n                                            kskToRetire = new List<DnssecPrivateKey>();\n\n                                        kskToRetire.Add(privateKey);\n                                    }\n                                    break;\n\n                                case DnssecPrivateKeyState.Retired:\n                                    //KSK needs to be revoked for RFC5011 consideration\n                                    if (DateTime.UtcNow > privateKey.StateTransitionBy)\n                                    {\n                                        //key has been retired for sufficient time\n                                        if (kskToRevoke is null)\n                                            kskToRevoke = new List<DnssecPrivateKey>();\n\n                                        kskToRevoke.Add(privateKey);\n                                    }\n                                    break;\n\n                                case DnssecPrivateKeyState.Revoked:\n                                    if (DateTime.UtcNow > privateKey.StateTransitionBy)\n                                    {\n                                        //key has been revoked for sufficient time\n                                        if (keysToUnpublish is null)\n                                            keysToUnpublish = new List<DnssecPrivateKey>();\n\n                                        keysToUnpublish.Add(privateKey);\n                                    }\n                                    break;\n                            }\n                        }\n                        else\n                        {\n                            //ZSK\n                            switch (privateKey.State)\n                            {\n                                case DnssecPrivateKeyState.Published:\n                                    if (DateTime.UtcNow > privateKey.StateTransitionBy)\n                                    {\n                                        //long enough time old RRset to expire from caches\n                                        privateKey.SetState(DnssecPrivateKeyState.Ready);\n\n                                        if (zskToActivate is null)\n                                            zskToActivate = new List<DnssecPrivateKey>();\n\n                                        zskToActivate.Add(privateKey);\n                                    }\n                                    break;\n\n                                case DnssecPrivateKeyState.Ready:\n                                    if (zskToActivate is null)\n                                        zskToActivate = new List<DnssecPrivateKey>();\n\n                                    zskToActivate.Add(privateKey);\n                                    break;\n\n                                case DnssecPrivateKeyState.Active:\n                                    if (privateKey.IsRetiring)\n                                    {\n                                        if (zskToRetire is null)\n                                            zskToRetire = new List<DnssecPrivateKey>();\n\n                                        zskToRetire.Add(privateKey);\n                                    }\n                                    else\n                                    {\n                                        if (privateKey.IsRolloverNeeded())\n                                        {\n                                            if (zskToRollover is null)\n                                                zskToRollover = new List<DnssecPrivateKey>();\n\n                                            zskToRollover.Add(privateKey);\n                                        }\n                                    }\n                                    break;\n\n                                case DnssecPrivateKeyState.Retired:\n                                    if (DateTime.UtcNow > privateKey.StateTransitionBy)\n                                    {\n                                        //key has been retired for sufficient time\n                                        if (keysToUnpublish is null)\n                                            keysToUnpublish = new List<DnssecPrivateKey>();\n\n                                        keysToUnpublish.Add(privateKey);\n                                    }\n                                    break;\n                            }\n                        }\n                    }\n                }\n\n                #region KSK actions\n\n                if (kskToReady is not null)\n                {\n                    string dnsKeyTags = null;\n\n                    foreach (DnssecPrivateKey kskPrivateKey in kskToReady)\n                    {\n                        kskPrivateKey.SetState(DnssecPrivateKeyState.Ready);\n\n                        if (kskToActivate is null)\n                            kskToActivate = new List<DnssecPrivateKey>();\n\n                        kskToActivate.Add(kskPrivateKey);\n\n                        if (dnsKeyTags is null)\n                            dnsKeyTags = kskPrivateKey.KeyTag.ToString();\n                        else\n                            dnsKeyTags += \", \" + kskPrivateKey.KeyTag.ToString();\n                    }\n\n                    saveZone = true;\n\n                    _dnsServer.LogManager.Write(\"The KSK DNSKEYs (\" + dnsKeyTags + \") from the primary zone are ready for changing the DS records at the parent zone: \" + ToString());\n                }\n\n                if (kskToActivate is not null)\n                {\n                    try\n                    {\n                        IReadOnlyList<DnssecPrivateKey> kskPrivateKeys = await GetDSPublishedPrivateKeysAsync(kskToActivate);\n                        if (kskPrivateKeys.Count > 0)\n                        {\n                            string dnsKeyTags = null;\n\n                            foreach (DnssecPrivateKey kskPrivateKey in kskPrivateKeys)\n                            {\n                                kskPrivateKey.SetState(DnssecPrivateKeyState.Active);\n\n                                if (dnsKeyTags is null)\n                                    dnsKeyTags = kskPrivateKey.KeyTag.ToString();\n                                else\n                                    dnsKeyTags += \", \" + kskPrivateKey.KeyTag.ToString();\n                            }\n\n                            saveZone = true;\n\n                            _dnsServer.LogManager.Write(\"The KSK DNSKEYs (\" + dnsKeyTags + \") from the primary zone were activated successfully: \" + ToString());\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n                    }\n                }\n\n                if (kskToRetire is not null)\n                {\n                    if (await RetireKskDnsKeysAsync(kskToRetire, false))\n                        saveZone = true;\n                }\n\n                if (kskToRevoke is not null)\n                {\n                    RevokeKskDnsKeys(kskToRevoke);\n                    saveZone = true;\n                }\n\n                #endregion\n\n                #region ZSK actions\n\n                if (zskToActivate is not null)\n                {\n                    ActivateZskDnsKeys(zskToActivate);\n                    saveZone = true;\n                }\n\n                if (zskToRetire is not null)\n                {\n                    if (RetireZskDnsKeys(zskToRetire, false))\n                        saveZone = true;\n                }\n\n                if (zskToRollover is not null)\n                {\n                    foreach (DnssecPrivateKey zskPrivateKey in zskToRollover)\n                        RolloverDnsKey(zskPrivateKey.KeyTag);\n\n                    saveZone = true;\n                }\n\n                #endregion\n\n                if (keysToUnpublish is not null)\n                {\n                    UnpublishDnsKeys(keysToUnpublish);\n                    saveZone = true;\n                }\n\n                //re-signing task\n                uint reSignPeriod = GetSignatureValidityPeriod() / 10; //the period when signature refresh check is done\n                if (DateTime.UtcNow > _lastSignatureRefreshCheckedOn.AddSeconds(reSignPeriod))\n                {\n                    if (TryRefreshAllSignatures())\n                        saveZone = true;\n\n                    _lastSignatureRefreshCheckedOn = DateTime.UtcNow;\n                }\n\n                if (saveZone)\n                    _dnsServer.AuthZoneManager.SaveZoneFile(_name);\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n            finally\n            {\n                Timer dnssecTimer = _dnssecTimer;\n                if (dnssecTimer is not null)\n                {\n                    lock (dnssecTimer)\n                    {\n                        dnssecTimer.Change(DNSSEC_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);\n                    }\n                }\n            }\n        }\n\n        public void SignZone(DnssecPrivateKey kskPrivateKey, DnssecPrivateKey zskPrivateKey, uint dnsKeyTtl, bool useNSec3, ushort iterations = 0, byte saltLength = 0)\n        {\n            if (kskPrivateKey.KeyType != DnssecPrivateKeyType.KeySigningKey)\n                throw new ArgumentException(\"The private key must be a Key Signing Key.\", nameof(kskPrivateKey));\n\n            if (zskPrivateKey.KeyType != DnssecPrivateKeyType.ZoneSigningKey)\n                throw new ArgumentException(\"The private key must be a Zone Signing Key.\", nameof(zskPrivateKey));\n\n            byte[] salt = null;\n\n            if (useNSec3)\n            {\n                if (saltLength > 32)\n                    throw new ArgumentOutOfRangeException(nameof(saltLength), \"NSEC3 salt length valid range is 0-32\");\n\n                if (saltLength > 0)\n                {\n                    salt = new byte[saltLength];\n                    RandomNumberGenerator.Fill(salt);\n                }\n                else\n                {\n                    salt = [];\n                }\n            }\n\n            SignZone([kskPrivateKey, zskPrivateKey], dnsKeyTtl, useNSec3, iterations, salt);\n        }\n\n        public void SignZone(IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys, uint dnsKeyTtl, bool useNSec3, ushort iterations = 0, byte[] salt = null)\n        {\n            //do validations\n            if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"Cannot sign zone: the zone is already signed.\");\n\n            if (useNSec3)\n            {\n                if (iterations > 50)\n                    throw new ArgumentOutOfRangeException(nameof(iterations), \"NSEC3 iterations valid range is 0-50\");\n\n                if (salt.Length > 32)\n                    throw new ArgumentOutOfRangeException(nameof(salt), \"NSEC3 salt length valid range is 0-32\");\n            }\n\n            bool foundKsk = false;\n            bool foundZsk = false;\n\n            foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)\n            {\n                switch (dnssecPrivateKey.KeyType)\n                {\n                    case DnssecPrivateKeyType.KeySigningKey:\n                        foundKsk = true;\n                        break;\n\n                    case DnssecPrivateKeyType.ZoneSigningKey:\n                        foundZsk = true;\n                        break;\n                }\n            }\n\n            if (!foundKsk)\n                throw new ArgumentException(\"The private keys must contain at least one Key Signing Key.\", nameof(dnssecPrivateKeys));\n\n            if (!foundZsk)\n                throw new ArgumentException(\"The private keys must contain at least one Zone Signing Key.\", nameof(dnssecPrivateKeys));\n\n            //load dnssec private keys\n            _dnssecPrivateKeys = new Dictionary<ushort, DnssecPrivateKey>(dnssecPrivateKeys.Count);\n\n            foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)\n                _dnssecPrivateKeys.Add(dnssecPrivateKey.KeyTag, dnssecPrivateKey);\n\n            //start zone signing\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            try\n            {\n                IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n                //find max record ttl in zone\n                uint maxRecordTtl = 0;\n\n                foreach (AuthZone zone in zones)\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in zone.Entries)\n                    {\n                        IReadOnlyList<DnsResourceRecord> rrset = entry.Value;\n\n                        //find min TTL\n                        uint rrsetTtl = 0;\n\n                        foreach (DnsResourceRecord rr in rrset)\n                        {\n                            if ((rrsetTtl == 0) || (rrsetTtl > rr.OriginalTtlValue))\n                                rrsetTtl = rr.OriginalTtlValue;\n                        }\n\n                        if (rrsetTtl > maxRecordTtl)\n                            maxRecordTtl = rrsetTtl;\n                    }\n                }\n\n                //update private key state                \n                uint propagationDelay = GetPropagationDelay();\n\n                foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                {\n                    DnssecPrivateKey privateKey = privateKeyEntry.Value;\n                    if (privateKey.State == DnssecPrivateKeyState.Generated)\n                    {\n                        switch (privateKey.KeyType)\n                        {\n                            case DnssecPrivateKeyType.KeySigningKey:\n                                privateKey.SetState(DnssecPrivateKeyState.Published, maxRecordTtl + propagationDelay);\n                                break;\n\n                            case DnssecPrivateKeyType.ZoneSigningKey:\n                                privateKey.SetState(DnssecPrivateKeyState.Ready);\n                                break;\n                        }\n                    }\n                }\n\n                //add DNSKEYs\n                List<DnsResourceRecord> dnsKeyRecords = new List<DnsResourceRecord>(_dnssecPrivateKeys.Count);\n\n                foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKey in _dnssecPrivateKeys)\n                    dnsKeyRecords.Add(new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.Value.DnsKey));\n\n                if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList<DnsResourceRecord> deletedDnsKeyRecords))\n                    throw new InvalidOperationException(\"Failed to add DNSKEY.\");\n\n                addedRecords.AddRange(dnsKeyRecords);\n                deletedRecords.AddRange(deletedDnsKeyRecords);\n\n                //sign all RRSets\n                foreach (AuthZone zone in zones)\n                {\n                    IReadOnlyList<DnsResourceRecord> newRRSigRecords = zone.SignAllRRSets();\n                    if (newRRSigRecords.Count > 0)\n                    {\n                        zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                        addedRecords.AddRange(newRRSigRecords);\n                        deletedRecords.AddRange(deletedRRSigRecords);\n                    }\n                }\n\n                if (useNSec3)\n                {\n                    EnableNSec3(zones, iterations, salt);\n                    _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3;\n                }\n                else\n                {\n                    EnableNSec(zones);\n                    _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC;\n                }\n\n                //update private key state\n                foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                {\n                    DnssecPrivateKey privateKey = privateKeyEntry.Value;\n                    switch (privateKey.KeyType)\n                    {\n                        case DnssecPrivateKeyType.ZoneSigningKey:\n                            if (privateKey.State == DnssecPrivateKeyState.Ready)\n                                privateKey.SetState(DnssecPrivateKeyState.Active);\n\n                            break;\n                    }\n                }\n\n                _dnssecTimer = new Timer(DnssecTimerCallback, null, DNSSEC_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n\n                CommitAndIncrementSerial(deletedRecords, addedRecords);\n                TriggerNotify();\n            }\n            catch\n            {\n                _dnssecStatus = AuthZoneDnssecStatus.Unsigned;\n                _dnssecPrivateKeys = null;\n\n                Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> addedRecordGroups = DnsResourceRecord.GroupRecords(addedRecords);\n\n                foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> addedRecordGroup in addedRecordGroups)\n                {\n                    AuthZone zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, addedRecordGroup.Key);\n\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> addedRecordEntry in addedRecordGroup.Value)\n                        zone.TryDeleteRecords(addedRecordEntry.Key, addedRecordEntry.Value, out _);\n                }\n\n                Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> deletedRecordGroups = DnsResourceRecord.GroupRecords(deletedRecords);\n\n                foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> deletedRecordGroup in deletedRecordGroups)\n                {\n                    AuthZone zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, deletedRecordGroup.Key);\n\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> deletedRecordEntry in deletedRecordGroup.Value)\n                    {\n                        foreach (DnsResourceRecord deletedRecord in deletedRecordEntry.Value)\n                            zone.AddRecord(deletedRecord, out _, out _);\n                    }\n                }\n\n                throw;\n            }\n        }\n\n        public void UnsignZone()\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"Cannot unsign zone: the is zone not signed.\");\n\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n            IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n            foreach (AuthZone zone in zones)\n            {\n                deletedRecords.AddRange(zone.RemoveAllDnssecRecords());\n\n                if (zone is SubDomainZone subDomainZone)\n                {\n                    if (zone.IsEmpty)\n                        _dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone\n                    else\n                        subDomainZone.AutoUpdateState();\n                }\n            }\n\n            Timer dnssecTimer = _dnssecTimer;\n            if (dnssecTimer is not null)\n            {\n                lock (dnssecTimer)\n                {\n                    dnssecTimer.Dispose();\n                    _dnssecTimer = null;\n                }\n            }\n\n            _dnssecPrivateKeys = null;\n            _dnssecStatus = AuthZoneDnssecStatus.Unsigned;\n\n            CommitAndIncrementSerial(deletedRecords);\n            TriggerNotify();\n        }\n\n        public void ConvertToNSec()\n        {\n            if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3)\n                throw new DnsServerException(\"Cannot convert to NSEC: the zone must be signed with NSEC3 for conversion.\");\n\n            lock (_dnssecUpdateLock)\n            {\n                IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n                DisableNSec3(zones);\n\n                //since zones were removed when disabling NSEC3; get updated non empty zones list\n                List<AuthZone> nonEmptyZones = new List<AuthZone>(zones.Count);\n\n                foreach (AuthZone zone in zones)\n                {\n                    if (!zone.IsEmpty)\n                        nonEmptyZones.Add(zone);\n                }\n\n                EnableNSec(nonEmptyZones);\n\n                _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC;\n            }\n\n            TriggerNotify();\n        }\n\n        public void ConvertToNSec3(ushort iterations, byte saltLength)\n        {\n            if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC)\n                throw new DnsServerException(\"Cannot convert to NSEC3: the zone must be signed with NSEC for conversion.\");\n\n            if (iterations > 50)\n                throw new ArgumentOutOfRangeException(nameof(iterations), \"NSEC3 iterations valid range is 0-50\");\n\n            if (saltLength > 32)\n                throw new ArgumentOutOfRangeException(nameof(saltLength), \"NSEC3 salt length valid range is 0-32\");\n\n            lock (_dnssecUpdateLock)\n            {\n                IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n                DisableNSec(zones);\n                EnableNSec3(zones, iterations, saltLength);\n\n                _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3;\n            }\n\n            TriggerNotify();\n        }\n\n        public void UpdateNSec3Parameters(ushort iterations, byte saltLength)\n        {\n            if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3)\n                throw new DnsServerException(\"Cannot update NSEC3 parameters: the zone must be signed with NSEC3 first.\");\n\n            if (iterations > 50)\n                throw new ArgumentOutOfRangeException(nameof(iterations), \"NSEC3 iterations valid range is 0-50\");\n\n            if (saltLength > 32)\n                throw new ArgumentOutOfRangeException(nameof(saltLength), \"NSEC3 salt length valid range is 0-32\");\n\n            lock (_dnssecUpdateLock)\n            {\n                IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n                DisableNSec3(zones);\n\n                //since zones were removed when disabling NSEC3; get updated non empty zones list\n                List<AuthZone> nonEmptyZones = new List<AuthZone>(zones.Count);\n\n                foreach (AuthZone zone in zones)\n                {\n                    if (!zone.IsEmpty)\n                        nonEmptyZones.Add(zone);\n                }\n\n                EnableNSec3(nonEmptyZones, iterations, saltLength);\n            }\n\n            TriggerNotify();\n        }\n\n        private void RefreshNSec()\n        {\n            lock (_dnssecUpdateLock)\n            {\n                IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n                EnableNSec(zones);\n            }\n        }\n\n        private void RefreshNSec3()\n        {\n            lock (_dnssecUpdateLock)\n            {\n                IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n                //get non NSEC3 zones\n                List<AuthZone> nonNSec3Zones = new List<AuthZone>(zones.Count);\n\n                foreach (AuthZone zone in zones)\n                {\n                    if (zone.HasOnlyNSec3Records())\n                        continue;\n\n                    nonNSec3Zones.Add(zone);\n                }\n\n                IReadOnlyList<DnsResourceRecord> nsec3ParamRecords = GetRecords(DnsResourceRecordType.NSEC3PARAM);\n                DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData;\n\n                EnableNSec3(nonNSec3Zones, nsec3Param.Iterations, nsec3Param.Salt);\n            }\n        }\n\n        private void EnableNSec(IReadOnlyList<AuthZone> zones)\n        {\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            uint ttl = GetZoneSoaMinimum();\n\n            for (int i = 0; i < zones.Count; i++)\n            {\n                AuthZone zone = zones[i];\n                AuthZone nextZone;\n\n                if (i < zones.Count - 1)\n                    nextZone = zones[i + 1];\n                else\n                    nextZone = zones[0];\n\n                IReadOnlyList<DnsResourceRecord> newNSecRecords = zone.GetUpdatedNSecRRSet(nextZone.Name, ttl);\n                if (newNSecRecords.Count > 0)\n                {\n                    if (!zone.TrySetRecords(DnsResourceRecordType.NSEC, newNSecRecords, out IReadOnlyList<DnsResourceRecord> deletedNSecRecords))\n                        throw new DnsServerException(\"Failed to set DNSSEC records. Please try again.\");\n\n                    addedRecords.AddRange(newNSecRecords);\n                    deletedRecords.AddRange(deletedNSecRecords);\n\n                    IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newNSecRecords);\n                    if (newRRSigRecords.Count > 0)\n                    {\n                        zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                        addedRecords.AddRange(newRRSigRecords);\n                        deletedRecords.AddRange(deletedRRSigRecords);\n                    }\n                }\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n        }\n\n        private void DisableNSec(IReadOnlyList<AuthZone> zones)\n        {\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            foreach (AuthZone zone in zones)\n                deletedRecords.AddRange(zone.RemoveNSecRecordsWithRRSig());\n\n            CommitAndIncrementSerial(deletedRecords);\n        }\n\n        private void EnableNSec3(IReadOnlyList<AuthZone> zones, ushort iterations, byte saltLength)\n        {\n            byte[] salt;\n\n            if (saltLength > 0)\n            {\n                salt = new byte[saltLength];\n                RandomNumberGenerator.Fill(salt);\n            }\n            else\n            {\n                salt = [];\n            }\n\n            EnableNSec3(zones, iterations, salt);\n        }\n\n        private void EnableNSec3(IReadOnlyList<AuthZone> zones, ushort iterations, byte[] salt)\n        {\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            List<DnsResourceRecord> partialNSec3Records = new List<DnsResourceRecord>(zones.Count);\n            int apexLabelCount = DnsRRSIGRecordData.GetLabelCount(_name);\n\n            uint ttl = GetZoneSoaMinimum();\n\n            //list all partial NSEC3 records\n            foreach (AuthZone zone in zones)\n            {\n                partialNSec3Records.Add(zone.GetPartialNSec3Record(_name, ttl, iterations, salt));\n\n                int zoneLabelCount = DnsRRSIGRecordData.GetLabelCount(zone.Name);\n                if (zone.Name.StartsWith(\"*.\"))\n                    zoneLabelCount++; //need to consider wildcard label for ENT detection\n\n                if ((zoneLabelCount - apexLabelCount) > 1)\n                {\n                    //empty non-terminal (ENT) may exists\n                    string currentOwnerName = zone.Name;\n\n                    while (true)\n                    {\n                        currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName);\n                        if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase))\n                            break;\n\n                        //add partial NSEC3 record for ENT\n                        AuthZone entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object\n                        partialNSec3Records.Add(entZone.GetPartialNSec3Record(_name, ttl, iterations, salt));\n                    }\n                }\n            }\n\n            //sort partial NSEC3 records\n            partialNSec3Records.Sort(delegate (DnsResourceRecord rr1, DnsResourceRecord rr2)\n            {\n                return string.CompareOrdinal(rr1.Name, rr2.Name);\n            });\n\n            //deduplicate partial NSEC3 records and insert next hashed owner name to complete them\n            List<DnsResourceRecord> uniqueNSec3Records = new List<DnsResourceRecord>(partialNSec3Records.Count);\n\n            for (int i = 0; i < partialNSec3Records.Count; i++)\n            {\n                DnsResourceRecord partialNSec3Record = partialNSec3Records[i];\n                DnsResourceRecord nextPartialNSec3Record;\n\n                if (i < partialNSec3Records.Count - 1)\n                {\n                    nextPartialNSec3Record = partialNSec3Records[i + 1];\n\n                    //check for duplicates\n                    if (partialNSec3Record.Name.Equals(nextPartialNSec3Record.Name, StringComparison.OrdinalIgnoreCase))\n                    {\n                        //found duplicate; merge current nsec3 into next nsec3\n                        DnsNSEC3RecordData nsec3 = partialNSec3Record.RDATA as DnsNSEC3RecordData;\n                        DnsNSEC3RecordData nextNSec3 = nextPartialNSec3Record.RDATA as DnsNSEC3RecordData;\n\n                        List<DnsResourceRecordType> uniqueTypes = new List<DnsResourceRecordType>(nsec3.Types.Count + nextNSec3.Types.Count);\n                        uniqueTypes.AddRange(nsec3.Types);\n\n                        foreach (DnsResourceRecordType type in nextNSec3.Types)\n                        {\n                            if (!uniqueTypes.Contains(type))\n                                uniqueTypes.Add(type);\n                        }\n\n                        uniqueTypes.Sort();\n\n                        //update the next nsec3 record and continue\n                        DnsNSEC3RecordData mergedPartialNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty<byte>(), uniqueTypes);\n                        partialNSec3Records[i + 1] = new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, mergedPartialNSec3);\n                        continue;\n                    }\n                }\n                else\n                {\n                    //for last NSEC3, next NSEC3 is the first in list\n                    nextPartialNSec3Record = partialNSec3Records[0];\n                }\n\n                //add NSEC3 record with next hashed owner name\n                {\n                    DnsNSEC3RecordData partialNSec3 = partialNSec3Record.RDATA as DnsNSEC3RecordData;\n                    byte[] nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(nextPartialNSec3Record.Name);\n\n                    DnsNSEC3RecordData updatedNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, nextHashedOwnerName, partialNSec3.Types);\n                    uniqueNSec3Records.Add(new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, updatedNSec3));\n                }\n            }\n\n            //insert and sign NSEC3 records\n            foreach (DnsResourceRecord uniqueNSec3Record in uniqueNSec3Records)\n            {\n                AuthZone zone = _dnsServer.AuthZoneManager.GetOrAddSubDomainZone(_name, uniqueNSec3Record.Name);\n\n                DnsResourceRecord[] newNSec3Records = new DnsResourceRecord[] { uniqueNSec3Record };\n\n                if (!zone.TrySetRecords(DnsResourceRecordType.NSEC3, newNSec3Records, out IReadOnlyList<DnsResourceRecord> deletedNSec3Records))\n                    throw new InvalidOperationException();\n\n                addedRecords.AddRange(newNSec3Records);\n                deletedRecords.AddRange(deletedNSec3Records);\n\n                IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newNSec3Records);\n                if (newRRSigRecords.Count > 0)\n                {\n                    zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                    addedRecords.AddRange(newRRSigRecords);\n                    deletedRecords.AddRange(deletedRRSigRecords);\n                }\n            }\n\n            //insert and sign NSEC3PARAM record\n            {\n                DnsNSEC3PARAMRecordData newNSec3Param = new DnsNSEC3PARAMRecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt);\n                DnsResourceRecord[] newNSec3ParamRecords = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NSEC3PARAM, DnsClass.IN, ttl, newNSec3Param) };\n\n                if (!TrySetRecords(DnsResourceRecordType.NSEC3PARAM, newNSec3ParamRecords, out IReadOnlyList<DnsResourceRecord> deletedNSec3ParamRecords))\n                    throw new InvalidOperationException();\n\n                addedRecords.AddRange(newNSec3ParamRecords);\n                deletedRecords.AddRange(deletedNSec3ParamRecords);\n\n                IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newNSec3ParamRecords);\n                if (newRRSigRecords.Count > 0)\n                {\n                    AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                    addedRecords.AddRange(newRRSigRecords);\n                    deletedRecords.AddRange(deletedRRSigRecords);\n                }\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n        }\n\n        private void DisableNSec3(IReadOnlyList<AuthZone> zones)\n        {\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            foreach (AuthZone zone in zones)\n            {\n                deletedRecords.AddRange(zone.RemoveNSec3RecordsWithRRSig());\n\n                if (zone is SubDomainZone subDomainZone)\n                {\n                    if (zone.IsEmpty)\n                        _dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone\n                    else\n                        subDomainZone.AutoUpdateState();\n                }\n            }\n\n            CommitAndIncrementSerial(deletedRecords);\n        }\n\n        public DnssecPrivateKey GenerateAndAddPrivateKey(DnssecPrivateKeyType keyType, DnssecAlgorithm algorithm, ushort rolloverDays, int keySize = -1)\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"The primary zone must be signed.\");\n\n            int i = 0;\n            while (i++ < 5)\n            {\n                DnssecPrivateKey privateKey = DnssecPrivateKey.Create(algorithm, keyType, keySize);\n                privateKey.RolloverDays = rolloverDays;\n\n                lock (_dnssecPrivateKeys)\n                {\n                    if (_dnssecPrivateKeys.TryAdd(privateKey.KeyTag, privateKey))\n                        return privateKey;\n                }\n            }\n\n            throw new DnsServerException(\"Failed to add private key: key tag collision. Please try again.\");\n        }\n\n        public void AddPrivateKey(DnssecPrivateKey privateKey)\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"The primary zone must be signed.\");\n\n            lock (_dnssecPrivateKeys)\n            {\n                if (!_dnssecPrivateKeys.TryAdd(privateKey.KeyTag, privateKey))\n                    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.\");\n            }\n        }\n\n        public DnssecPrivateKey UpdatePrivateKey(ushort keyTag, ushort rolloverDays)\n        {\n            lock (_dnssecPrivateKeys)\n            {\n                if (!_dnssecPrivateKeys.TryGetValue(keyTag, out DnssecPrivateKey privateKey))\n                    throw new DnsServerException(\"Cannot update private key: no such private key was found.\");\n\n                privateKey.RolloverDays = rolloverDays;\n\n                return privateKey;\n            }\n        }\n\n        public void DeletePrivateKey(ushort keyTag)\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"The zone must be signed.\");\n\n            lock (_dnssecPrivateKeys)\n            {\n                if (!_dnssecPrivateKeys.TryGetValue(keyTag, out DnssecPrivateKey privateKey))\n                    throw new DnsServerException(\"Cannot delete private key: no such private key was found.\");\n\n                if (privateKey.State != DnssecPrivateKeyState.Generated)\n                    throw new DnsServerException(\"Cannot delete private key: only keys with Generated state can be deleted.\");\n\n                _dnssecPrivateKeys.Remove(keyTag);\n            }\n        }\n\n        public void PublishAllGeneratedKeys()\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"The zone must be signed.\");\n\n            List<DnssecPrivateKey> generatedPrivateKeys = new List<DnssecPrivateKey>();\n            List<DnsResourceRecord> newDnsKeyRecords = new List<DnsResourceRecord>();\n\n            uint dnsKeyTtl = GetDnsKeyTtl();\n\n            lock (_dnssecPrivateKeys)\n            {\n                foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                {\n                    DnssecPrivateKey privateKey = privateKeyEntry.Value;\n\n                    if (privateKey.State == DnssecPrivateKeyState.Generated)\n                    {\n                        generatedPrivateKeys.Add(privateKey);\n                        newDnsKeyRecords.Add(new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.DnsKey));\n                    }\n                }\n            }\n\n            if (generatedPrivateKeys.Count == 0)\n                throw new DnsServerException(\"Cannot publish DNSKEY: no generated private keys were found.\");\n\n            IReadOnlyList<DnsResourceRecord> dnsKeyRecords = _entries.AddOrUpdate(DnsResourceRecordType.DNSKEY, delegate (DnsResourceRecordType key)\n            {\n                return newDnsKeyRecords;\n            },\n            delegate (DnsResourceRecordType key, IReadOnlyList<DnsResourceRecord> existingRecords)\n            {\n                foreach (DnsResourceRecord existingRecord in existingRecords)\n                {\n                    foreach (DnsResourceRecord newDnsKeyRecord in newDnsKeyRecords)\n                    {\n                        if (existingRecord.Equals(newDnsKeyRecord))\n                            throw new DnsServerException(\"Cannot publish DNSKEY: the key is already published.\");\n                    }\n                }\n\n                List<DnsResourceRecord> dnsKeyRecords = new List<DnsResourceRecord>(existingRecords.Count + newDnsKeyRecords.Count);\n\n                dnsKeyRecords.AddRange(existingRecords);\n                dnsKeyRecords.AddRange(newDnsKeyRecords);\n\n                return dnsKeyRecords;\n            });\n\n            //update private key state before signing\n            uint propagationDelay = GetPropagationDelay();\n\n            foreach (DnssecPrivateKey privateKey in generatedPrivateKeys)\n                privateKey.SetState(DnssecPrivateKeyState.Published, dnsKeyTtl + propagationDelay);\n\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            addedRecords.AddRange(newDnsKeyRecords);\n\n            IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(dnsKeyRecords);\n            if (newRRSigRecords.Count > 0)\n            {\n                AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                addedRecords.AddRange(newRRSigRecords);\n                deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n            TriggerNotify();\n        }\n\n        private void ActivateZskDnsKeys(IReadOnlyList<DnssecPrivateKey> zskPrivateKeys)\n        {\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            //re-sign all records with new private keys\n            IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n            foreach (AuthZone zone in zones)\n            {\n                IReadOnlyList<DnsResourceRecord> newRRSigRecords = zone.SignAllRRSets();\n                if (newRRSigRecords.Count > 0)\n                {\n                    zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                    addedRecords.AddRange(newRRSigRecords);\n                    deletedRecords.AddRange(deletedRRSigRecords);\n                }\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n            TriggerNotify();\n\n            //update private key state\n            string dnsKeyTags = null;\n\n            foreach (DnssecPrivateKey privateKey in zskPrivateKeys)\n            {\n                privateKey.SetState(DnssecPrivateKeyState.Active);\n\n                if (dnsKeyTags is null)\n                    dnsKeyTags = privateKey.KeyTag.ToString();\n                else\n                    dnsKeyTags += \", \" + privateKey.KeyTag.ToString();\n            }\n\n            _dnsServer.LogManager.Write(\"The ZSK DNSKEYs (\" + dnsKeyTags + \") from the primary zone were activated successfully: \" + ToString());\n        }\n\n        public void RolloverDnsKey(ushort keyTag)\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"The zone must be signed.\");\n\n            DnssecPrivateKey privateKey;\n\n            lock (_dnssecPrivateKeys)\n            {\n                if (!_dnssecPrivateKeys.TryGetValue(keyTag, out privateKey))\n                    throw new DnsServerException(\"Cannot rollover private key: no such private key was found.\");\n            }\n\n            switch (privateKey.State)\n            {\n                case DnssecPrivateKeyState.Ready:\n                case DnssecPrivateKeyState.Active:\n                    if (privateKey.IsRetiring)\n                        throw new DnsServerException(\"Cannot rollover private key: the private key is already set to retire.\");\n\n                    break;\n\n                default:\n                    throw new DnsServerException(\"Cannot rollover private key: the private key state must be Ready or Active to be able to rollover.\");\n            }\n\n            switch (privateKey.Algorithm)\n            {\n                case DnssecAlgorithm.RSAMD5:\n                case DnssecAlgorithm.RSASHA1:\n                case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:\n                case DnssecAlgorithm.RSASHA256:\n                case DnssecAlgorithm.RSASHA512:\n                    GenerateAndAddPrivateKey(privateKey.KeyType, privateKey.Algorithm, privateKey.RolloverDays, (privateKey as DnssecRsaPrivateKey).KeySize);\n                    break;\n\n                case DnssecAlgorithm.ECDSAP256SHA256:\n                case DnssecAlgorithm.ECDSAP384SHA384:\n                case DnssecAlgorithm.ED25519:\n                case DnssecAlgorithm.ED448:\n                    GenerateAndAddPrivateKey(privateKey.KeyType, privateKey.Algorithm, privateKey.RolloverDays);\n                    break;\n\n                default:\n                    throw new NotSupportedException(\"DNSSEC algorithm is not supported: \" + privateKey.Algorithm.ToString());\n            }\n\n            PublishAllGeneratedKeys();\n            privateKey.SetToRetire();\n        }\n\n        public async Task RetireDnsKeyAsync(ushort keyTag)\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"The zone must be signed.\");\n\n            DnssecPrivateKey privateKeyToRetire;\n\n            lock (_dnssecPrivateKeys)\n            {\n                if (!_dnssecPrivateKeys.TryGetValue(keyTag, out privateKeyToRetire))\n                    throw new DnsServerException(\"Cannot retire private key: no such private key was found.\");\n            }\n\n            switch (privateKeyToRetire.KeyType)\n            {\n                case DnssecPrivateKeyType.KeySigningKey:\n                    switch (privateKeyToRetire.State)\n                    {\n                        case DnssecPrivateKeyState.Ready:\n                        case DnssecPrivateKeyState.Active:\n                            if (!await RetireKskDnsKeysAsync([privateKeyToRetire], true))\n                                throw new DnsServerException(\"Cannot retire private key: no successor key was found to safely retire the key.\");\n\n                            break;\n\n                        default:\n                            throw new DnsServerException(\"Cannot retire private key: the KSK private key state must be Ready or Active to be able to retire.\");\n                    }\n                    break;\n\n                case DnssecPrivateKeyType.ZoneSigningKey:\n                    switch (privateKeyToRetire.State)\n                    {\n                        case DnssecPrivateKeyState.Active:\n                            if (!RetireZskDnsKeys(new DnssecPrivateKey[] { privateKeyToRetire }, true))\n                                throw new DnsServerException(\"Cannot retire private key: no successor key was found to safely retire the key.\");\n\n                            break;\n\n                        default:\n                            throw new DnsServerException(\"Cannot retire private key: the ZSK private key state must be Active to be able to retire.\");\n                    }\n                    break;\n\n                default:\n                    throw new InvalidOperationException();\n            }\n        }\n\n        private async Task<bool> RetireKskDnsKeysAsync(IReadOnlyList<DnssecPrivateKey> kskPrivateKeys, bool ignoreAlgorithm)\n        {\n            string dnsKeyTags = null;\n            uint dsTtl = 0;\n            uint parentSidePropagationDelay = 0;\n\n            foreach (DnssecPrivateKey kskPrivateKey in kskPrivateKeys)\n            {\n                bool isSafeToRetire = false;\n\n                lock (_dnssecPrivateKeys)\n                {\n                    foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                    {\n                        DnssecPrivateKey privateKey = privateKeyEntry.Value;\n\n                        if ((privateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (privateKey.KeyTag != kskPrivateKey.KeyTag) && !privateKey.IsRetiring)\n                        {\n                            if (ignoreAlgorithm)\n                            {\n                                //manual retire case\n                                if (privateKey.Algorithm != kskPrivateKey.Algorithm)\n                                {\n                                    //check if the sucessor ksk has a matching zsk\n                                    bool foundMatchingZsk = false;\n\n                                    foreach (KeyValuePair<ushort, DnssecPrivateKey> zskPrivateKeyEntry in _dnssecPrivateKeys)\n                                    {\n                                        DnssecPrivateKey zskPrivateKey = zskPrivateKeyEntry.Value;\n\n                                        if ((zskPrivateKey.KeyType == DnssecPrivateKeyType.ZoneSigningKey) && (zskPrivateKey.Algorithm == privateKey.Algorithm) && (zskPrivateKey.State == DnssecPrivateKeyState.Active) && !zskPrivateKey.IsRetiring)\n                                        {\n                                            foundMatchingZsk = true;\n                                            break;\n                                        }\n                                    }\n\n                                    if (!foundMatchingZsk)\n                                        continue;\n                                }\n                            }\n                            else\n                            {\n                                //rollover case\n                                if (privateKey.Algorithm != kskPrivateKey.Algorithm)\n                                    continue;\n                            }\n\n                            if (privateKey.State == DnssecPrivateKeyState.Active)\n                            {\n                                isSafeToRetire = true;\n                                break;\n                            }\n\n                            if ((privateKey.State == DnssecPrivateKeyState.Ready) && (kskPrivateKey.State == DnssecPrivateKeyState.Ready))\n                            {\n                                isSafeToRetire = true;\n                                break;\n                            }\n                        }\n                    }\n                }\n\n                if (isSafeToRetire)\n                {\n                    if (dsTtl == 0)\n                        dsTtl = await GetDSTtlAsync();\n\n                    if (parentSidePropagationDelay == 0)\n                        parentSidePropagationDelay = await GetParentSidePropagationDelayAsync();\n\n                    kskPrivateKey.SetState(DnssecPrivateKeyState.Retired, dsTtl + parentSidePropagationDelay);\n\n                    if (dnsKeyTags is null)\n                        dnsKeyTags = kskPrivateKey.KeyTag.ToString();\n                    else\n                        dnsKeyTags += \", \" + kskPrivateKey.KeyTag.ToString();\n                }\n            }\n\n            if (dnsKeyTags is not null)\n            {\n                _dnsServer.LogManager.Write(\"The KSK DNSKEYs (\" + dnsKeyTags + \") from the primary zone were retired successfully: \" + ToString());\n\n                return true;\n            }\n\n            return false;\n        }\n\n        private bool RetireZskDnsKeys(IReadOnlyList<DnssecPrivateKey> zskPrivateKeys, bool ignoreAlgorithm)\n        {\n            string dnsKeyTags = null;\n            List<DnssecPrivateKey> zskToDeactivate = null;\n            uint maxRRSigTtl = 0;\n            uint propagationDelay = 0;\n\n            foreach (DnssecPrivateKey zskPrivateKey in zskPrivateKeys)\n            {\n                bool isSafeToRetire = false;\n\n                lock (_dnssecPrivateKeys)\n                {\n                    foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                    {\n                        DnssecPrivateKey privateKey = privateKeyEntry.Value;\n\n                        if ((privateKey.KeyType == DnssecPrivateKeyType.ZoneSigningKey) && (privateKey.KeyTag != zskPrivateKey.KeyTag) && (privateKey.State == DnssecPrivateKeyState.Active) && !privateKey.IsRetiring)\n                        {\n                            if (ignoreAlgorithm)\n                            {\n                                //manual retire case\n                                if (privateKey.Algorithm != zskPrivateKey.Algorithm)\n                                {\n                                    //check if the sucessor zsk has a matching ksk\n                                    bool foundMatchingKsk = false;\n\n                                    foreach (KeyValuePair<ushort, DnssecPrivateKey> kskPrivateKeyEntry in _dnssecPrivateKeys)\n                                    {\n                                        DnssecPrivateKey kskPrivateKey = kskPrivateKeyEntry.Value;\n\n                                        if ((kskPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (kskPrivateKey.Algorithm == privateKey.Algorithm) && ((kskPrivateKey.State == DnssecPrivateKeyState.Ready) || (kskPrivateKey.State == DnssecPrivateKeyState.Active)) && !kskPrivateKey.IsRetiring)\n                                        {\n                                            foundMatchingKsk = true;\n                                            break;\n                                        }\n                                    }\n\n                                    if (!foundMatchingKsk)\n                                        continue;\n                                }\n                            }\n                            else\n                            {\n                                //rollover case\n                                if (privateKey.Algorithm != zskPrivateKey.Algorithm)\n                                    continue;\n                            }\n\n                            isSafeToRetire = true;\n                            break;\n                        }\n                    }\n                }\n\n                if (isSafeToRetire)\n                {\n                    if (maxRRSigTtl == 0)\n                        maxRRSigTtl = GetMaxRRSigTtl();\n\n                    if (propagationDelay == 0)\n                        propagationDelay = GetPropagationDelay();\n\n                    zskPrivateKey.SetState(DnssecPrivateKeyState.Retired, maxRRSigTtl + propagationDelay);\n\n                    if (zskToDeactivate is null)\n                        zskToDeactivate = new List<DnssecPrivateKey>();\n\n                    zskToDeactivate.Add(zskPrivateKey);\n\n                    if (dnsKeyTags is null)\n                        dnsKeyTags = zskPrivateKey.KeyTag.ToString();\n                    else\n                        dnsKeyTags += \", \" + zskPrivateKey.KeyTag.ToString();\n                }\n            }\n\n            if (zskToDeactivate is not null)\n                DeactivateZskDnsKeys(zskToDeactivate);\n\n            if (dnsKeyTags is not null)\n            {\n                _dnsServer.LogManager.Write(\"The ZSK DNSKEYs (\" + dnsKeyTags + \") from the primary zone were retired successfully: \" + ToString());\n\n                return true;\n            }\n\n            return false;\n        }\n\n        private void DeactivateZskDnsKeys(IReadOnlyList<DnssecPrivateKey> zskPrivateKeys)\n        {\n            //remove all RRSIGs for the DNSKEYs\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n            foreach (AuthZone zone in zones)\n            {\n                IReadOnlyList<DnsResourceRecord> rrsigRecords = zone.GetRecords(DnsResourceRecordType.RRSIG);\n                List<DnsResourceRecord> rrsigsToRemove = new List<DnsResourceRecord>();\n\n                foreach (DnsResourceRecord rrsigRecord in rrsigRecords)\n                {\n                    DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;\n\n                    foreach (DnssecPrivateKey privateKey in zskPrivateKeys)\n                    {\n                        if (rrsig.KeyTag == privateKey.KeyTag)\n                        {\n                            rrsigsToRemove.Add(rrsigRecord);\n                            break;\n                        }\n                    }\n                }\n\n                if (zone.TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords))\n                    deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            CommitAndIncrementSerial(deletedRecords);\n            TriggerNotify();\n\n            string dnsKeyTags = null;\n\n            foreach (DnssecPrivateKey privateKey in zskPrivateKeys)\n            {\n                if (dnsKeyTags is null)\n                    dnsKeyTags = privateKey.KeyTag.ToString();\n                else\n                    dnsKeyTags += \", \" + privateKey.KeyTag.ToString();\n            }\n\n            _dnsServer.LogManager.Write(\"The ZSK DNSKEYs (\" + dnsKeyTags + \") from the primary zone were deactivated successfully: \" + ToString());\n        }\n\n        private void RevokeKskDnsKeys(List<DnssecPrivateKey> kskPrivateKeys)\n        {\n            if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList<DnsResourceRecord> existingDnsKeyRecords))\n                throw new InvalidOperationException();\n\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            List<DnsResourceRecord> dnsKeyRecords = new List<DnsResourceRecord>();\n\n            foreach (DnsResourceRecord existingDnsKeyRecord in existingDnsKeyRecords)\n            {\n                bool found = false;\n\n                foreach (DnssecPrivateKey privateKey in kskPrivateKeys)\n                {\n                    if (existingDnsKeyRecord.RDATA.Equals(privateKey.DnsKey))\n                    {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found)\n                    dnsKeyRecords.Add(existingDnsKeyRecord);\n            }\n\n            uint dnsKeyTtl = existingDnsKeyRecords[0].OriginalTtlValue;\n            List<ushort> keyTagsToRemove = new List<ushort>(kskPrivateKeys.Count);\n\n            //rfc7583#section-3.3.4\n            //modifiedQueryInterval = MAX(1hr, MIN(15 days, TTLkey / 2)) \n            uint modifiedQueryInterval = Math.Max(3600u, Math.Min(15 * 24 * 60 * 60, dnsKeyTtl / 2));\n\n            foreach (DnssecPrivateKey privateKey in kskPrivateKeys)\n            {\n                keyTagsToRemove.Add(privateKey.KeyTag);\n                privateKey.SetState(DnssecPrivateKeyState.Revoked, modifiedQueryInterval);\n\n                DnsResourceRecord revokedDnsKeyRecord = new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.DnsKey);\n                dnsKeyRecords.Add(revokedDnsKeyRecord);\n            }\n\n            if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList<DnsResourceRecord> deletedDnsKeyRecords))\n                throw new InvalidOperationException();\n\n            addedRecords.AddRange(dnsKeyRecords);\n            deletedRecords.AddRange(deletedDnsKeyRecords);\n\n            IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(dnsKeyRecords);\n            if (newRRSigRecords.Count > 0)\n            {\n                AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                addedRecords.AddRange(newRRSigRecords);\n                deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            //remove RRSIG for removed keys\n            {\n                IReadOnlyList<DnsResourceRecord> rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG);\n                List<DnsResourceRecord> rrsigsToRemove = new List<DnsResourceRecord>();\n\n                foreach (DnsResourceRecord rrsigRecord in rrsigRecords)\n                {\n                    DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;\n                    if (rrsig.TypeCovered != DnsResourceRecordType.DNSKEY)\n                        continue;\n\n                    foreach (ushort keyTagToRemove in keyTagsToRemove)\n                    {\n                        if (rrsig.KeyTag == keyTagToRemove)\n                        {\n                            rrsigsToRemove.Add(rrsigRecord);\n                            break;\n                        }\n                    }\n                }\n\n                if (TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords))\n                    deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n            TriggerNotify();\n\n            //update revoked private keys\n            string dnsKeyTags = null;\n\n            lock (_dnssecPrivateKeys)\n            {\n                //remove old entry\n                foreach (ushort keyTag in keyTagsToRemove)\n                {\n                    if (_dnssecPrivateKeys.Remove(keyTag))\n                    {\n                        if (dnsKeyTags is null)\n                            dnsKeyTags = keyTag.ToString();\n                        else\n                            dnsKeyTags += \", \" + keyTag.ToString();\n                    }\n                }\n\n                //add new entry\n                foreach (DnssecPrivateKey privateKey in kskPrivateKeys)\n                    _dnssecPrivateKeys.Add(privateKey.KeyTag, privateKey);\n            }\n\n            _dnsServer.LogManager.Write(\"The KSK DNSKEYs (\" + dnsKeyTags + \") from the primary zone were revoked successfully: \" + ToString());\n        }\n\n        private void UnpublishDnsKeys(IReadOnlyList<DnssecPrivateKey> deadPrivateKeys)\n        {\n            if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList<DnsResourceRecord> existingDnsKeyRecords))\n                throw new InvalidOperationException();\n\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            List<DnsResourceRecord> dnsKeyRecords = new List<DnsResourceRecord>();\n\n            foreach (DnsResourceRecord existingDnsKeyRecord in existingDnsKeyRecords)\n            {\n                bool found = false;\n\n                foreach (DnssecPrivateKey privateKey in deadPrivateKeys)\n                {\n                    if (existingDnsKeyRecord.RDATA.Equals(privateKey.DnsKey))\n                    {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found)\n                    dnsKeyRecords.Add(existingDnsKeyRecord);\n            }\n\n            if (dnsKeyRecords.Count < 2)\n                throw new InvalidOperationException();\n\n            if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList<DnsResourceRecord> deletedDnsKeyRecords))\n                throw new InvalidOperationException();\n\n            addedRecords.AddRange(dnsKeyRecords);\n            deletedRecords.AddRange(deletedDnsKeyRecords);\n\n            IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(dnsKeyRecords);\n            if (newRRSigRecords.Count > 0)\n            {\n                AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                addedRecords.AddRange(newRRSigRecords);\n                deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            //remove RRSig for revoked keys\n            {\n                IReadOnlyList<DnsResourceRecord> rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG);\n                List<DnsResourceRecord> rrsigsToRemove = new List<DnsResourceRecord>();\n\n                foreach (DnsResourceRecord rrsigRecord in rrsigRecords)\n                {\n                    DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;\n                    if (rrsig.TypeCovered != DnsResourceRecordType.DNSKEY)\n                        continue;\n\n                    foreach (DnssecPrivateKey privateKey in deadPrivateKeys)\n                    {\n                        if (rrsig.KeyTag == privateKey.KeyTag)\n                        {\n                            rrsigsToRemove.Add(rrsigRecord);\n                            break;\n                        }\n                    }\n                }\n\n                if (TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords))\n                    deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n            TriggerNotify();\n\n            //remove private keys permanently\n            string dnsKeyTags = null;\n\n            lock (_dnssecPrivateKeys)\n            {\n                foreach (DnssecPrivateKey privateKey in deadPrivateKeys)\n                {\n                    if (_dnssecPrivateKeys.Remove(privateKey.KeyTag))\n                    {\n                        if (dnsKeyTags is null)\n                            dnsKeyTags = privateKey.KeyTag.ToString();\n                        else\n                            dnsKeyTags += \", \" + privateKey.KeyTag.ToString();\n                    }\n                }\n            }\n\n            _dnsServer.LogManager.Write(\"The DNSKEYs (\" + dnsKeyTags + \") from the primary zone were unpublished successfully: \" + ToString());\n        }\n\n        private async Task<IReadOnlyList<DnssecPrivateKey>> GetDSPublishedPrivateKeysAsync(IReadOnlyList<DnssecPrivateKey> privateKeys, CancellationToken cancellationToken = default)\n        {\n            if (_name.Length == 0)\n                return privateKeys; //zone is root\n\n            //delete any existing DS entries from cache to allow resolving latest ones\n            _dnsServer.CacheZoneManager.DeleteZone(_name);\n\n            DirectDnsClient dnsClient = new DirectDnsClient(_dnsServer);\n            dnsClient.DnssecValidation = true;\n            dnsClient.Timeout = 10000;\n\n            IReadOnlyList<DnsDSRecordData> dsRecords;\n\n            try\n            {\n                dsRecords = DnsClient.ParseResponseDS(await dnsClient.ResolveAsync(new DnsQuestionRecord(_name, DnsResourceRecordType.DS, DnsClass.IN), cancellationToken: cancellationToken));\n            }\n            catch\n            {\n                //suppress exception here to avoid filling log file\n                return [];\n            }\n\n            List<DnssecPrivateKey> activePrivateKeys = new List<DnssecPrivateKey>(dsRecords.Count);\n\n            foreach (DnsDSRecordData dsRecord in dsRecords)\n            {\n                foreach (DnssecPrivateKey privateKey in privateKeys)\n                {\n                    if ((dsRecord.KeyTag == privateKey.DnsKey.ComputedKeyTag) && (dsRecord.Algorithm == privateKey.DnsKey.Algorithm) && privateKey.DnsKey.IsDnsKeyValid(_name, dsRecord))\n                    {\n                        activePrivateKeys.Add(privateKey);\n                        break;\n                    }\n                }\n            }\n\n            return activePrivateKeys;\n        }\n\n        private bool TryRefreshAllSignatures()\n        {\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            IReadOnlyList<AuthZone> zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name);\n\n            foreach (AuthZone zone in zones)\n            {\n                IReadOnlyList<DnsResourceRecord> newRRSigRecords = zone.RefreshSignatures();\n                if (newRRSigRecords.Count > 0)\n                {\n                    zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                    addedRecords.AddRange(newRRSigRecords);\n                    deletedRecords.AddRange(deletedRRSigRecords);\n                }\n            }\n\n            if ((deletedRecords.Count > 0) || (addedRecords.Count > 0))\n            {\n                CommitAndIncrementSerial(deletedRecords, addedRecords);\n                TriggerNotify();\n\n                return true;\n            }\n\n            return false;\n        }\n\n        internal override IReadOnlyList<DnsResourceRecord> SignRRSet(IReadOnlyList<DnsResourceRecord> records)\n        {\n            DnsResourceRecordType rrsetType = records[0].Type;\n\n            List<DnsResourceRecord> rrsigRecords = new List<DnsResourceRecord>();\n            uint signatureValidityPeriod = GetSignatureValidityPeriod();\n\n            switch (rrsetType)\n            {\n                case DnsResourceRecordType.DNSKEY:\n                    lock (_dnssecPrivateKeys)\n                    {\n                        foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                        {\n                            DnssecPrivateKey privateKey = privateKeyEntry.Value;\n                            if (privateKey.KeyType != DnssecPrivateKeyType.KeySigningKey)\n                                continue;\n\n                            switch (privateKey.State)\n                            {\n                                case DnssecPrivateKeyState.Published:\n                                case DnssecPrivateKeyState.Ready:\n                                case DnssecPrivateKeyState.Active:\n                                case DnssecPrivateKeyState.Revoked:\n                                    rrsigRecords.Add(privateKey.SignRRSet(_name, records, DNSSEC_SIGNATURE_INCEPTION_OFFSET, signatureValidityPeriod));\n                                    break;\n                            }\n                        }\n                    }\n                    break;\n\n                case DnsResourceRecordType.RRSIG:\n                    throw new InvalidOperationException();\n\n                case DnsResourceRecordType.ANAME:\n                case DnsResourceRecordType.APP:\n                    throw new DnsServerException(\"Cannot sign RRSet: The record type [\" + rrsetType.ToString() + \"] is not supported by DNSSEC signed primary zones.\");\n\n                default:\n                    if ((rrsetType == DnsResourceRecordType.NS) && (records[0].Name.Length > _name.Length))\n                        return Array.Empty<DnsResourceRecord>(); //referrer NS records are not signed\n\n                    foreach (DnsResourceRecord record in records)\n                    {\n                        if (record.GetAuthGenericRecordInfo().Disabled)\n                            throw new DnsServerException(\"Cannot sign RRSet: Signing disabled records is not supported.\");\n                    }\n\n                    lock (_dnssecPrivateKeys)\n                    {\n                        foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                        {\n                            DnssecPrivateKey privateKey = privateKeyEntry.Value;\n                            if (privateKey.KeyType != DnssecPrivateKeyType.ZoneSigningKey)\n                                continue;\n\n                            switch (privateKey.State)\n                            {\n                                case DnssecPrivateKeyState.Ready:\n                                case DnssecPrivateKeyState.Active:\n                                    rrsigRecords.Add(privateKey.SignRRSet(_name, records, DNSSEC_SIGNATURE_INCEPTION_OFFSET, signatureValidityPeriod));\n                                    break;\n                            }\n                        }\n                    }\n                    break;\n            }\n\n            if (rrsigRecords.Count == 0)\n                throw new InvalidOperationException(\"Cannot sign RRSet: no private key was available.\");\n\n            return rrsigRecords;\n        }\n\n        internal void UpdateDnssecRecordsFor(AuthZone zone, DnsResourceRecordType type)\n        {\n            //lock to sync this call to prevent inconsistent NSEC/NSEC3 updates\n            lock (_dnssecUpdateLock)\n            {\n                IReadOnlyList<DnsResourceRecord> records = zone.GetRecords(type);\n                if (records.Count > 0)\n                {\n                    //rrset added or updated\n                    //sign rrset\n                    IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(records);\n                    if (newRRSigRecords.Count > 0)\n                    {\n                        zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                        CommitAndIncrementSerial(deletedRRSigRecords, newRRSigRecords);\n                    }\n                }\n                else\n                {\n                    //rrset deleted\n                    //delete rrsig\n                    IReadOnlyList<DnsResourceRecord> existingRRSigRecords = zone.GetRecords(DnsResourceRecordType.RRSIG);\n                    if (existingRRSigRecords.Count > 0)\n                    {\n                        List<DnsResourceRecord> recordsToDelete = new List<DnsResourceRecord>();\n\n                        foreach (DnsResourceRecord existingRRSigRecord in existingRRSigRecords)\n                        {\n                            DnsRRSIGRecordData rrsig = existingRRSigRecord.RDATA as DnsRRSIGRecordData;\n                            if (rrsig.TypeCovered == type)\n                                recordsToDelete.Add(existingRRSigRecord);\n                        }\n\n                        if (zone.TryDeleteRecords(DnsResourceRecordType.RRSIG, recordsToDelete, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords))\n                            CommitAndIncrementSerial(deletedRRSigRecords);\n                    }\n                }\n\n                if (_dnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC)\n                {\n                    UpdateNSecRRSetFor(zone);\n                }\n                else\n                {\n                    UpdateNSec3RRSetFor(zone);\n\n                    int apexLabelCount = DnsRRSIGRecordData.GetLabelCount(_name);\n                    int zoneLabelCount = DnsRRSIGRecordData.GetLabelCount(zone.Name);\n                    if (zone.Name.StartsWith(\"*.\") || zone.Name.Equals('*'))\n                        zoneLabelCount++; //need to consider wildcard label for ENT detection\n\n                    if ((zoneLabelCount - apexLabelCount) > 1)\n                    {\n                        //empty non-terminal (ENT) may exists\n                        string currentOwnerName = zone.Name;\n\n                        while (true)\n                        {\n                            currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName);\n                            if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase))\n                                break;\n\n                            //update NSEC3 rrset for current owner name\n                            AuthZone entZone = _dnsServer.AuthZoneManager.GetAuthZone(_name, currentOwnerName);\n                            if (entZone is null)\n                                entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object\n\n                            UpdateNSec3RRSetFor(entZone);\n                        }\n                    }\n                }\n            }\n        }\n\n        private void UpdateNSecRRSetFor(AuthZone zone)\n        {\n            uint ttl = GetZoneSoaMinimum();\n\n            IReadOnlyList<DnsResourceRecord> newNSecRecords = GetUpdatedNSecRRSetFor(zone, ttl);\n            if (newNSecRecords.Count > 0)\n            {\n                DnsResourceRecord newNSecRecord = newNSecRecords[0];\n                DnsNSECRecordData newNSec = newNSecRecord.RDATA as DnsNSECRecordData;\n                if (newNSec.Types.Count == 2)\n                {\n                    //only NSEC and RRSIG exists so remove NSEC\n                    IReadOnlyList<DnsResourceRecord> deletedNSecRecords = zone.RemoveNSecRecordsWithRRSig();\n                    if (deletedNSecRecords.Count > 0)\n                        CommitAndIncrementSerial(deletedNSecRecords);\n\n                    //relink previous nsec\n                    RelinkPreviousNSecRRSetFor(newNSecRecord, ttl, true);\n                }\n                else\n                {\n                    List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n                    List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n                    if (!zone.TrySetRecords(DnsResourceRecordType.NSEC, newNSecRecords, out IReadOnlyList<DnsResourceRecord> deletedNSecRecords))\n                        throw new DnsServerException(\"Failed to set DNSSEC records. Please try again.\");\n\n                    addedRecords.AddRange(newNSecRecords);\n                    deletedRecords.AddRange(deletedNSecRecords);\n\n                    IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newNSecRecords);\n                    if (newRRSigRecords.Count > 0)\n                    {\n                        zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                        addedRecords.AddRange(newRRSigRecords);\n                        deletedRecords.AddRange(deletedRRSigRecords);\n                    }\n\n                    CommitAndIncrementSerial(deletedRecords, addedRecords);\n\n                    if (deletedNSecRecords.Count == 0)\n                    {\n                        //new NSEC created since no old NSEC was removed\n                        //relink previous nsec\n                        RelinkPreviousNSecRRSetFor(newNSecRecord, ttl, false);\n                    }\n                }\n            }\n        }\n\n        private void UpdateNSec3RRSetFor(AuthZone zone)\n        {\n            uint ttl = GetZoneSoaMinimum();\n            bool noSubDomainExistsForEmptyZone = (zone.IsEmpty || zone.HasOnlyNSec3Records()) && !_dnsServer.AuthZoneManager.SubDomainExistsFor(_name, zone.Name);\n\n            IReadOnlyList<DnsResourceRecord> newNSec3Records = GetUpdatedNSec3RRSetFor(zone, ttl, noSubDomainExistsForEmptyZone);\n            if (newNSec3Records.Count > 0)\n            {\n                DnsResourceRecord newNSec3Record = newNSec3Records[0];\n\n                AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetOrAddSubDomainZone(_name, newNSec3Record.Name);\n                if (nsec3Zone is null)\n                    throw new InvalidOperationException();\n\n                if (noSubDomainExistsForEmptyZone)\n                {\n                    //no records exists in real zone and no sub domain exists, so remove NSEC3\n                    IReadOnlyList<DnsResourceRecord> deletedNSec3Records = nsec3Zone.RemoveNSec3RecordsWithRRSig();\n                    if (deletedNSec3Records.Count > 0)\n                        CommitAndIncrementSerial(deletedNSec3Records);\n\n                    //remove nsec3 sub domain zone if empty since it wont get removed otherwise\n                    if (nsec3Zone is SubDomainZone nsec3SubDomainZone)\n                    {\n                        if (nsec3Zone.IsEmpty)\n                            _dnsServer.AuthZoneManager.RemoveSubDomainZone(nsec3Zone.Name); //remove empty sub zone\n                        else\n                            nsec3SubDomainZone.AutoUpdateState();\n                    }\n\n                    //remove the real zone if empty so that any of the ENT that exists can also be removed later\n                    if (zone is SubDomainZone subDomainZone)\n                    {\n                        if (zone.IsEmpty)\n                            _dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone\n                        else\n                            subDomainZone.AutoUpdateState();\n                    }\n\n                    //relink previous nsec3\n                    RelinkPreviousNSec3RRSet(newNSec3Record, ttl, true);\n                }\n                else\n                {\n                    List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n                    List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n                    if (!nsec3Zone.TrySetRecords(DnsResourceRecordType.NSEC3, newNSec3Records, out IReadOnlyList<DnsResourceRecord> deletedNSec3Records))\n                        throw new DnsServerException(\"Failed to set DNSSEC records. Please try again.\");\n\n                    addedRecords.AddRange(newNSec3Records);\n                    deletedRecords.AddRange(deletedNSec3Records);\n\n                    IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newNSec3Records);\n                    if (newRRSigRecords.Count > 0)\n                    {\n                        nsec3Zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                        addedRecords.AddRange(newRRSigRecords);\n                        deletedRecords.AddRange(deletedRRSigRecords);\n                    }\n\n                    CommitAndIncrementSerial(deletedRecords, addedRecords);\n\n                    if (deletedNSec3Records.Count == 0)\n                    {\n                        //new NSEC3 created since no old NSEC3 was removed\n                        //relink previous nsec\n                        RelinkPreviousNSec3RRSet(newNSec3Record, ttl, false);\n                    }\n                }\n            }\n        }\n\n        private IReadOnlyList<DnsResourceRecord> GetUpdatedNSecRRSetFor(AuthZone zone, uint ttl)\n        {\n            AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(_name, zone.Name);\n            if (nextZone is null)\n                nextZone = this;\n\n            return zone.GetUpdatedNSecRRSet(nextZone.Name, ttl);\n        }\n\n        private IReadOnlyList<DnsResourceRecord> GetUpdatedNSec3RRSetFor(AuthZone zone, uint ttl, bool forceGetNewRRSet)\n        {\n            if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3PARAM, out IReadOnlyList<DnsResourceRecord> nsec3ParamRecords))\n                throw new InvalidOperationException();\n\n            DnsResourceRecord nsec3ParamRecord = nsec3ParamRecords[0];\n            DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecord.RDATA as DnsNSEC3PARAMRecordData;\n\n            string hashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(zone.Name) + (_name.Length > 0 ? \".\" + _name : \"\");\n            byte[] nextHashedOwnerName = null;\n\n            //find next hashed owner name\n            string currentOwnerName = hashedOwnerName;\n\n            while (true)\n            {\n                AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(_name, currentOwnerName);\n                if (nextZone is null)\n                    break;\n\n                IReadOnlyList<DnsResourceRecord> nextNSec3Records = nextZone.GetRecords(DnsResourceRecordType.NSEC3);\n                if (nextNSec3Records.Count > 0)\n                {\n                    nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(nextNSec3Records[0].Name);\n                    break;\n                }\n\n                currentOwnerName = nextZone.Name;\n            }\n\n            if (nextHashedOwnerName is null)\n            {\n                //didnt find next NSEC3 record since current must be last; find the first NSEC3 record\n                DnsResourceRecord previousNSec3Record = null;\n\n                while (true)\n                {\n                    AuthZone previousZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentOwnerName);\n                    if (previousZone is null)\n                        break;\n\n                    IReadOnlyList<DnsResourceRecord> previousNSec3Records = previousZone.GetRecords(DnsResourceRecordType.NSEC3);\n                    if (previousNSec3Records.Count > 0)\n                        previousNSec3Record = previousNSec3Records[0];\n\n                    currentOwnerName = previousZone.Name;\n                }\n\n                if (previousNSec3Record is not null)\n                    nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(previousNSec3Record.Name);\n            }\n\n            if (nextHashedOwnerName is null)\n                nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(hashedOwnerName); //only 1 NSEC3 record in zone\n\n            IReadOnlyList<DnsResourceRecord> newNSec3Records = zone.CreateNSec3RRSet(hashedOwnerName, nextHashedOwnerName, ttl, nsec3Param.Iterations, nsec3Param.Salt);\n\n            if (forceGetNewRRSet)\n                return newNSec3Records;\n\n            AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, hashedOwnerName);\n            if (nsec3Zone is null)\n                return newNSec3Records;\n\n            return nsec3Zone.GetUpdatedNSec3RRSet(newNSec3Records);\n        }\n\n        private void RelinkPreviousNSecRRSetFor(DnsResourceRecord currentNSecRecord, uint ttl, bool wasRemoved)\n        {\n            AuthZone previousNsecZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentNSecRecord.Name);\n            if (previousNsecZone is null)\n                return; //current zone is apex\n\n            IReadOnlyList<DnsResourceRecord> newPreviousNSecRecords;\n\n            if (wasRemoved)\n                newPreviousNSecRecords = previousNsecZone.GetUpdatedNSecRRSet((currentNSecRecord.RDATA as DnsNSECRecordData).NextDomainName, ttl);\n            else\n                newPreviousNSecRecords = previousNsecZone.GetUpdatedNSecRRSet(currentNSecRecord.Name, ttl);\n\n            if (newPreviousNSecRecords.Count > 0)\n            {\n                if (!previousNsecZone.TrySetRecords(DnsResourceRecordType.NSEC, newPreviousNSecRecords, out IReadOnlyList<DnsResourceRecord> deletedNSecRecords))\n                    throw new DnsServerException(\"Failed to set DNSSEC records. Please try again.\");\n\n                List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n                List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n                addedRecords.AddRange(newPreviousNSecRecords);\n                deletedRecords.AddRange(deletedNSecRecords);\n\n                IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newPreviousNSecRecords);\n                if (newRRSigRecords.Count > 0)\n                {\n                    previousNsecZone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                    addedRecords.AddRange(newRRSigRecords);\n                    deletedRecords.AddRange(deletedRRSigRecords);\n                }\n\n                CommitAndIncrementSerial(deletedRecords, addedRecords);\n            }\n        }\n\n        private void RelinkPreviousNSec3RRSet(DnsResourceRecord currentNSec3Record, uint ttl, bool wasRemoved)\n        {\n            DnsNSEC3RecordData currentNSec3 = currentNSec3Record.RDATA as DnsNSEC3RecordData;\n\n            //find the previous NSEC3 and update it\n            DnsResourceRecord previousNSec3Record = null;\n            AuthZone previousNSec3Zone;\n            string currentOwnerName = currentNSec3Record.Name;\n\n            while (true)\n            {\n                previousNSec3Zone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentOwnerName);\n                if (previousNSec3Zone is null)\n                    break;\n\n                IReadOnlyList<DnsResourceRecord> previousNSec3Records = previousNSec3Zone.GetRecords(DnsResourceRecordType.NSEC3);\n                if (previousNSec3Records.Count > 0)\n                {\n                    previousNSec3Record = previousNSec3Records[0];\n                    break;\n                }\n\n                currentOwnerName = previousNSec3Zone.Name;\n            }\n\n            if (previousNSec3Record is null)\n            {\n                //didnt find previous NSEC3; find the last NSEC3 to update\n                if (wasRemoved)\n                    currentOwnerName = currentNSec3.NextHashedOwnerName + (_name.Length > 0 ? \".\" + _name : \"\");\n                else\n                    currentOwnerName = currentNSec3Record.Name;\n\n                while (true)\n                {\n                    AuthZone nextNSec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, currentOwnerName);\n                    if (nextNSec3Zone is null)\n                        break;\n\n                    IReadOnlyList<DnsResourceRecord> nextNSec3Records = nextNSec3Zone.GetRecords(DnsResourceRecordType.NSEC3);\n                    if (nextNSec3Records.Count > 0)\n                    {\n                        previousNSec3Record = nextNSec3Records[0];\n                        previousNSec3Zone = nextNSec3Zone;\n\n                        string nextHashedOwnerNameString = (previousNSec3Record.RDATA as DnsNSEC3RecordData).NextHashedOwnerName + (_name.Length > 0 ? \".\" + _name : \"\");\n                        if (DnsNSECRecordData.CanonicalComparison(previousNSec3Record.Name, nextHashedOwnerNameString) >= 0)\n                            break; //found last NSEC3\n\n                        //jump to next hashed owner\n                        currentOwnerName = nextHashedOwnerNameString;\n                    }\n                    else\n                    {\n                        currentOwnerName = nextNSec3Zone.Name;\n                    }\n                }\n            }\n\n            if (previousNSec3Record is null)\n                throw new InvalidOperationException();\n\n            DnsNSEC3RecordData previousNSec3 = previousNSec3Record.RDATA as DnsNSEC3RecordData;\n            DnsNSEC3RecordData newPreviousNSec3;\n\n            if (wasRemoved)\n                newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, currentNSec3.NextHashedOwnerNameValue, previousNSec3.Types);\n            else\n                newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, DnsNSEC3RecordData.GetHashedOwnerNameFrom(currentNSec3Record.Name), previousNSec3.Types);\n\n            DnsResourceRecord[] newPreviousNSec3Records = new DnsResourceRecord[] { new DnsResourceRecord(previousNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newPreviousNSec3) };\n\n            if (!previousNSec3Zone.TrySetRecords(DnsResourceRecordType.NSEC3, newPreviousNSec3Records, out IReadOnlyList<DnsResourceRecord> deletedNSec3Records))\n                throw new DnsServerException(\"Failed to set DNSSEC records. Please try again.\");\n\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            addedRecords.AddRange(newPreviousNSec3Records);\n            deletedRecords.AddRange(deletedNSec3Records);\n\n            IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newPreviousNSec3Records);\n            if (newRRSigRecords.Count > 0)\n            {\n                previousNSec3Zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                addedRecords.AddRange(newRRSigRecords);\n                deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n        }\n\n        private uint GetSignatureValidityPeriod()\n        {\n            //SOA EXPIRE * 2\n            return GetZoneSoaExpire() * 2;\n        }\n\n        private uint GetPropagationDelay()\n        {\n            //the max time required to sync zone changes to secondaries if NOTIFY fails to trigger a zone transfer\n            DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;\n            return soa.Refresh + soa.Retry;\n        }\n\n        private async Task<uint> GetParentSidePropagationDelayAsync(CancellationToken cancellationToken = default)\n        {\n            uint parentSidePropagationDelay = 24 * 60 * 60;\n\n            try\n            {\n                string parent = AuthZoneManager.GetParentZone(_name);\n                if (parent is null)\n                    parent = \"\";\n\n                DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(parent, DnsResourceRecordType.SOA, DnsClass.IN), 10000, cancellationToken: cancellationToken);\n                if (soaResponse.RCODE == DnsResponseCode.NoError)\n                {\n                    IReadOnlyList<DnsResourceRecord> records;\n\n                    if (soaResponse.Answer.Count > 0)\n                        records = soaResponse.Answer;\n                    else if (soaResponse.Authority.Count > 0)\n                        records = soaResponse.Authority;\n                    else\n                        records = null;\n\n                    if (records is not null)\n                    {\n                        foreach (DnsResourceRecord record in records)\n                        {\n                            if (record.Type == DnsResourceRecordType.SOA)\n                            {\n                                DnsSOARecordData parentSoa = record.RDATA as DnsSOARecordData;\n                                parentSidePropagationDelay = parentSoa.Refresh + parentSoa.Retry;\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n\n            return parentSidePropagationDelay;\n        }\n\n        private uint GetMaxRRSigTtl()\n        {\n            uint maxTtl = 0;\n\n            foreach (AuthZone zone in _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name))\n            {\n                if (!zone.Entries.TryGetValue(DnsResourceRecordType.RRSIG, out IReadOnlyList<DnsResourceRecord> rrsigRecords))\n                    continue;\n\n                foreach (DnsResourceRecord rr in rrsigRecords)\n                {\n                    if (rr.OriginalTtlValue > maxTtl)\n                        maxTtl = rr.OriginalTtlValue;\n                }\n            }\n\n            return maxTtl;\n        }\n\n        private async Task<uint> GetDSTtlAsync(CancellationToken cancellationToken = default)\n        {\n            uint dsTtl = 24 * 60 * 60;\n\n            try\n            {\n                DnsDatagram dsResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(_name, DnsResourceRecordType.DS, DnsClass.IN), 10000, cancellationToken: cancellationToken);\n                if (dsResponse.RCODE == DnsResponseCode.NoError)\n                {\n                    if (dsResponse.Answer.Count > 0)\n                    {\n                        //find min TTL\n                        dsTtl = 0;\n\n                        foreach (DnsResourceRecord answer in dsResponse.Answer)\n                        {\n                            if (answer.Type == DnsResourceRecordType.DS)\n                            {\n                                if ((dsTtl == 0) || (dsTtl > answer.OriginalTtlValue))\n                                    dsTtl = answer.OriginalTtlValue;\n                            }\n                        }\n                    }\n                    else\n                    {\n                        dsTtl = 0; //no DS was found\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(ex);\n            }\n\n            return dsTtl;\n        }\n\n        public uint GetDnsKeyTtl()\n        {\n            if (_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList<DnsResourceRecord> dnsKeyRecords))\n                return dnsKeyRecords[0].OriginalTtlValue;\n\n            return 24 * 60 * 60;\n        }\n\n        public void UpdateDnsKeyTtl(uint dnsKeyTtl)\n        {\n            if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                throw new DnsServerException(\"The zone must be signed.\");\n\n            lock (_dnssecPrivateKeys)\n            {\n                foreach (KeyValuePair<ushort, DnssecPrivateKey> privateKeyEntry in _dnssecPrivateKeys)\n                {\n                    switch (privateKeyEntry.Value.State)\n                    {\n                        case DnssecPrivateKeyState.Ready:\n                        case DnssecPrivateKeyState.Active:\n                            break;\n\n                        default:\n                            throw new DnsServerException(\"Cannot update DNSKEY TTL value: one or more private keys have state other than Ready or Active.\");\n                    }\n                }\n            }\n\n            if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList<DnsResourceRecord> dnsKeyRecords))\n                throw new InvalidOperationException();\n\n            DnsResourceRecord[] newDnsKeyRecords = new DnsResourceRecord[dnsKeyRecords.Count];\n\n            for (int i = 0; i < dnsKeyRecords.Count; i++)\n            {\n                DnsResourceRecord dnsKeyRecord = dnsKeyRecords[i];\n                newDnsKeyRecords[i] = new DnsResourceRecord(dnsKeyRecord.Name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, dnsKeyRecord.RDATA);\n            }\n\n            List<DnsResourceRecord> addedRecords = new List<DnsResourceRecord>();\n            List<DnsResourceRecord> deletedRecords = new List<DnsResourceRecord>();\n\n            if (!TrySetRecords(DnsResourceRecordType.DNSKEY, newDnsKeyRecords, out IReadOnlyList<DnsResourceRecord> deletedDnsKeyRecords))\n                throw new DnsServerException(\"Failed to update DNSKEY TTL. Please try again.\");\n\n            addedRecords.AddRange(newDnsKeyRecords);\n            deletedRecords.AddRange(deletedDnsKeyRecords);\n\n            IReadOnlyList<DnsResourceRecord> newRRSigRecords = SignRRSet(newDnsKeyRecords);\n            if (newRRSigRecords.Count > 0)\n            {\n                AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList<DnsResourceRecord> deletedRRSigRecords);\n\n                addedRecords.AddRange(newRRSigRecords);\n                deletedRecords.AddRange(deletedRRSigRecords);\n            }\n\n            CommitAndIncrementSerial(deletedRecords, addedRecords);\n            TriggerNotify();\n        }\n\n        #endregion\n\n        #region versioning\n\n        internal override void CommitAndIncrementSerial(IReadOnlyList<DnsResourceRecord> deletedRecords = null, IReadOnlyList<DnsResourceRecord> addedRecords = null)\n        {\n            if (_internal)\n            {\n                _lastModified = DateTime.UtcNow;\n                return;\n            }\n\n            base.CommitAndIncrementSerial(deletedRecords, addedRecords);\n        }\n\n        #endregion\n\n        #region public\n\n        public override string GetZoneTypeName()\n        {\n            return \"Primary\";\n        }\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n            {\n                switch (type)\n                {\n                    case DnsResourceRecordType.ANAME:\n                    case DnsResourceRecordType.APP:\n                        throw new DnsServerException(\"The record type is not supported by DNSSEC signed primary zones.\");\n\n                    default:\n                        foreach (DnsResourceRecord record in records)\n                        {\n                            if (record.GetAuthGenericRecordInfo().Disabled)\n                                throw new DnsServerException(\"Cannot set records: disabling records in a signed zones is not supported.\");\n                        }\n\n                        break;\n                }\n            }\n\n            switch (type)\n            {\n                case DnsResourceRecordType.CNAME:\n                case DnsResourceRecordType.DS:\n                    throw new InvalidOperationException(\"Cannot set \" + type.ToString() + \" record at zone apex.\");\n\n                case DnsResourceRecordType.SOA:\n                    if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase))\n                        throw new InvalidOperationException(\"Invalid SOA record.\");\n\n                    DnsResourceRecord newSoaRecord = records[0];\n                    DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData;\n\n                    if (newSoaRecord.OriginalTtlValue > newSoa.Expire)\n                        throw new DnsServerException(\"Cannot set record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (newSoa.Retry > newSoa.Refresh)\n                        throw new DnsServerException(\"Cannot set record: SOA RETRY cannot be greater than SOA REFRESH.\");\n\n                    if (newSoa.Refresh > newSoa.Expire)\n                        throw new DnsServerException(\"Cannot set record: SOA REFRESH cannot be greater than SOA EXPIRE.\");\n\n                    //remove any record info except serial date scheme and comments\n                    bool useSoaSerialDateScheme;\n                    string comments;\n                    {\n                        SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo();\n\n                        useSoaSerialDateScheme = recordInfo.UseSoaSerialDateScheme;\n                        comments = recordInfo.Comments;\n                    }\n\n                    newSoaRecord.Tag = null; //remove old record info\n\n                    {\n                        SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo();\n\n                        recordInfo.UseSoaSerialDateScheme = useSoaSerialDateScheme;\n                        recordInfo.Comments = comments;\n                        recordInfo.LastModified = DateTime.UtcNow;\n                    }\n\n                    uint oldSoaMinimum = GetZoneSoaMinimum();\n\n                    //setting new SOA\n                    if (_internal)\n                        _entries[DnsResourceRecordType.SOA] = records; //update SOA directly\n                    else\n                        CommitAndIncrementSerial(null, records);\n\n                    if (oldSoaMinimum != newSoa.Minimum)\n                    {\n                        switch (_dnssecStatus)\n                        {\n                            case AuthZoneDnssecStatus.SignedWithNSEC:\n                                RefreshNSec();\n                                break;\n\n                            case AuthZoneDnssecStatus.SignedWithNSEC3:\n                                RefreshNSec3();\n                                break;\n                        }\n                    }\n\n                    TriggerNotify();\n                    break;\n\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot set DNSSEC records.\");\n\n                case DnsResourceRecordType.FWD:\n                    throw new DnsServerException(\"The record type is not supported by primary zones.\");\n\n                default:\n                    if (records[0].OriginalTtlValue > GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot set records: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TrySetRecords(type, records, out IReadOnlyList<DnsResourceRecord> deletedRecords))\n                        throw new DnsServerException(\"Cannot set records. Please try again.\");\n\n                    CommitAndIncrementSerial(deletedRecords, records);\n\n                    if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                        UpdateDnssecRecordsFor(this, type);\n\n                    TriggerNotify();\n                    break;\n            }\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n            {\n                switch (record.Type)\n                {\n                    case DnsResourceRecordType.ANAME:\n                    case DnsResourceRecordType.APP:\n                        throw new DnsServerException(\"The record type is not supported by DNSSEC signed primary zones.\");\n\n                    default:\n                        if (record.GetAuthGenericRecordInfo().Disabled)\n                            throw new DnsServerException(\"Cannot add record: disabling records in a signed zones is not supported.\");\n\n                        break;\n                }\n            }\n\n            switch (record.Type)\n            {\n                case DnsResourceRecordType.APP:\n                    throw new InvalidOperationException(\"Cannot add record: use SetRecords() for \" + record.Type.ToString() + \" record\");\n\n                case DnsResourceRecordType.DS:\n                    throw new InvalidOperationException(\"Cannot set DS record at zone apex.\");\n\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot add DNSSEC record.\");\n\n                case DnsResourceRecordType.FWD:\n                    throw new DnsServerException(\"The record type is not supported by primary zones.\");\n\n                default:\n                    if (record.OriginalTtlValue > GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot add record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    AddRecord(record, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    if (addedRecords.Count > 0)\n                    {\n                        CommitAndIncrementSerial(deletedRecords, addedRecords);\n\n                        if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                            UpdateDnssecRecordsFor(this, record.Type);\n\n                        TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot delete SOA record.\");\n\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot delete DNSSEC records.\");\n\n                default:\n                    if (_entries.TryRemove(type, out IReadOnlyList<DnsResourceRecord> removedRecords))\n                    {\n                        CommitAndIncrementSerial(removedRecords);\n\n                        if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                            UpdateDnssecRecordsFor(this, type);\n\n                        TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata)\n        {\n            switch (type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot delete SOA record.\");\n\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot delete DNSSEC records.\");\n\n                default:\n                    if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord))\n                    {\n                        CommitAndIncrementSerial([deletedRecord]);\n\n                        if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                            UpdateDnssecRecordsFor(this, type);\n\n                        TriggerNotify();\n\n                        return true;\n                    }\n\n                    return false;\n            }\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            switch (oldRecord.Type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot update record: use SetRecords() for \" + oldRecord.Type.ToString() + \" record\");\n\n                case DnsResourceRecordType.DNSKEY:\n                case DnsResourceRecordType.RRSIG:\n                case DnsResourceRecordType.NSEC:\n                case DnsResourceRecordType.NSEC3PARAM:\n                case DnsResourceRecordType.NSEC3:\n                    throw new InvalidOperationException(\"Cannot update DNSSEC records.\");\n\n                default:\n                    if (oldRecord.Type != newRecord.Type)\n                        throw new InvalidOperationException(\"Old and new record types do not match.\");\n\n                    if ((_dnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.GetAuthGenericRecordInfo().Disabled)\n                        throw new DnsServerException(\"Cannot update record: disabling records in a signed zones is not supported.\");\n\n                    if (newRecord.OriginalTtlValue > GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot update record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord))\n                        throw new DnsServerException(\"Cannot update record: the record does not exists to be updated.\");\n\n                    AddRecord(newRecord, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    List<DnsResourceRecord> allDeletedRecords = new List<DnsResourceRecord>(deletedRecords.Count + 1);\n                    allDeletedRecords.Add(deletedRecord);\n                    allDeletedRecords.AddRange(deletedRecords);\n\n                    CommitAndIncrementSerial(allDeletedRecords, addedRecords);\n\n                    if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)\n                        UpdateDnssecRecordsFor(this, oldRecord.Type);\n\n                    TriggerNotify();\n                    break;\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool Disabled\n        {\n            get { return base.Disabled; }\n            set\n            {\n                if (base.Disabled == value)\n                    return;\n\n                base.Disabled = value; //set value early to be able to use it for notify\n\n                if (value)\n                    DisableNotifyTimer();\n                else\n                    TriggerNotify();\n            }\n        }\n\n        public override AuthZoneTransfer ZoneTransfer\n        {\n            get { return base.ZoneTransfer; }\n            set\n            {\n                if (_internal)\n                    throw new InvalidOperationException();\n\n                base.ZoneTransfer = value;\n            }\n        }\n\n        public override AuthZoneNotify Notify\n        {\n            get { return base.Notify; }\n            set\n            {\n                if (_internal)\n                    throw new InvalidOperationException();\n\n                switch (value)\n                {\n                    case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones:\n                        throw new ArgumentException(\"The Notify option is invalid for \" + GetZoneTypeName() + \" zones: \" + value.ToString(), nameof(Notify));\n                }\n\n                base.Notify = value;\n            }\n        }\n\n        public override AuthZoneUpdate Update\n        {\n            get { return base.Update; }\n            set\n            {\n                if (_internal)\n                    throw new InvalidOperationException();\n\n                base.Update = value;\n            }\n        }\n\n        public bool Internal\n        { get { return _internal; } }\n\n        public IReadOnlyCollection<DnssecPrivateKey> DnssecPrivateKeys\n        {\n            get\n            {\n                if (_dnssecPrivateKeys is null)\n                    return null;\n\n                lock (_dnssecPrivateKeys)\n                {\n                    return _dnssecPrivateKeys.Values;\n                }\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/SecondaryCatalogSubDomainZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class SecondaryCatalogSubDomainZone : SecondarySubDomainZone\n    {\n        #region constructor\n\n        public SecondaryCatalogSubDomainZone(SecondaryZone secondaryZone, string name)\n            : base(secondaryZone, name)\n        { }\n\n        #endregion\n\n        #region public\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            return []; //secondary catalog zone is not queriable\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/SecondaryCatalogZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class SecondaryCatalogZone : SecondaryForwarderZone\n    {\n        #region events\n\n        public event EventHandler<SecondaryCatalogEventArgs> ZoneAdded;\n        public event EventHandler<SecondaryCatalogEventArgs> ZoneRemoved;\n\n        #endregion\n\n        #region variables\n\n        readonly static IReadOnlyCollection<NetworkAccessControl> _allowACL =\n            [\n                new NetworkAccessControl(IPAddress.Any, 0),\n                new NetworkAccessControl(IPAddress.IPv6Any, 0)\n            ];\n\n        readonly static IReadOnlyCollection<NetworkAccessControl> _queryAccessAllowOnlyPrivateNetworksACL =\n            [\n                new NetworkAccessControl(IPAddress.Parse(\"127.0.0.0\"), 8),\n                new NetworkAccessControl(IPAddress.Parse(\"10.0.0.0\"), 8),\n                new NetworkAccessControl(IPAddress.Parse(\"100.64.0.0\"), 10),\n                new NetworkAccessControl(IPAddress.Parse(\"169.254.0.0\"), 16),\n                new NetworkAccessControl(IPAddress.Parse(\"172.16.0.0\"), 12),\n                new NetworkAccessControl(IPAddress.Parse(\"192.168.0.0\"), 16),\n                new NetworkAccessControl(IPAddress.Parse(\"2000::\"), 3, true),\n                new NetworkAccessControl(IPAddress.IPv6Any, 0)\n            ];\n\n        readonly static IReadOnlyCollection<NetworkAccessControl> _allowOnlyZoneNameServersACL =\n            [\n                new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32)\n            ];\n\n        readonly static IReadOnlyCollection<NetworkAccessControl> _denyACL =\n            [\n                new NetworkAccessControl(IPAddress.Parse(\"127.0.0.0\"), 8),\n                new NetworkAccessControl(IPAddress.Parse(\"::1\"), 128)\n            ];\n\n        readonly static NetworkAccessControl _allowZoneNameServersAndUseSpecifiedNetworkACL = new NetworkAccessControl(IPAddress.Parse(\"224.0.0.0\"), 32);\n\n        Dictionary<string, string> _membersIndex = new Dictionary<string, string>();\n\n        #endregion\n\n        #region constructor\n\n        public SecondaryCatalogZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(dnsServer, zoneInfo)\n        { }\n\n        public SecondaryCatalogZone(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null)\n            : base(dnsServer, name, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName)\n        { }\n\n        #endregion\n\n        #region protected\n\n        protected override void InitZone()\n        {\n            //init secondary catalog zone with dummy SOA and NS records\n            DnsSOARecordData soa = new DnsSOARecordData(\"invalid\", \"invalid\", 0, 300, 60, 604800, 900);\n            DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);\n            soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n            _entries[DnsResourceRecordType.SOA] = [soaRecord];\n            _entries[DnsResourceRecordType.NS] = [new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, 0, new DnsNSRecordData(\"invalid\"))];\n        }\n\n        #endregion\n\n        #region internal\n\n        internal void BuildMembersIndex()\n        {\n            Dictionary<string, string> membersIndex = new Dictionary<string, string>();\n\n            foreach (KeyValuePair<string, string> memberEntry in EnumerateCatalogMemberZones(_dnsServer))\n                membersIndex.TryAdd(memberEntry.Key.ToLowerInvariant(), memberEntry.Value);\n\n            _membersIndex = membersIndex;\n        }\n\n        #endregion\n\n        #region secondary catalog\n\n        public IReadOnlyCollection<string> GetAllMemberZoneNames()\n        {\n            return _membersIndex.Keys;\n        }\n\n        protected override async Task FinalizeZoneTransferAsync()\n        {\n            //secondary catalog does not maintain zone history; no need to call base method\n            string version = GetVersion();\n            if ((version is null) || !version.Equals(\"2\", StringComparison.OrdinalIgnoreCase))\n            {\n                _dnsServer.LogManager.Write(\"Failed to provision Secondary Catalog zone '\" + ToString() + \"': catalog version not supported.\");\n                return;\n            }\n\n            Dictionary<string, string> updatedMembersIndex = new Dictionary<string, string>();\n\n            foreach (KeyValuePair<string, string> memberEntry in EnumerateCatalogMemberZones(_dnsServer))\n                updatedMembersIndex.TryAdd(memberEntry.Key, memberEntry.Value);\n\n            Dictionary<string, object> membersToRemove = new Dictionary<string, object>();\n            Dictionary<string, string> membersToAdd = new Dictionary<string, string>();\n\n            foreach (KeyValuePair<string, string> memberEntry in _membersIndex)\n            {\n                if (!updatedMembersIndex.TryGetValue(memberEntry.Key, out string updatedMembersZoneDomain))\n                {\n                    //member was removed from catalog zone; remove local zone\n                    membersToRemove.Add(memberEntry.Key, null);\n                }\n                else if (!memberEntry.Value.Equals(updatedMembersZoneDomain, StringComparison.OrdinalIgnoreCase))\n                {\n                    //member exists but label does not match; reprovision zone\n                    membersToRemove.Add(memberEntry.Key, null);\n                    membersToAdd.Add(memberEntry.Key, updatedMembersZoneDomain);\n                }\n            }\n\n            foreach (KeyValuePair<string, string> updatedMemberEntry in updatedMembersIndex)\n            {\n                if (_membersIndex.TryGetValue(updatedMemberEntry.Key, out _))\n                {\n                    ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(updatedMemberEntry.Key);\n                    if (apexZone is not null)\n                        continue; //zone already exists; do nothing\n                }\n\n                //member was added to catalog zone; provision zone\n                membersToAdd.TryAdd(updatedMemberEntry.Key, updatedMemberEntry.Value);\n            }\n\n            //set global custom properties\n            UpdateGlobalAllowQueryProperty();\n            UpdateGlobalAllowTransferAndTsigKeyNamesProperties();\n\n            //add and remove member zones\n            if ((membersToRemove.Count > 0) || (membersToAdd.Count > 0))\n                await AddAndRemoveMemberZonesAsync(membersToRemove, membersToAdd);\n\n            //set member zone custom properties\n            if (updatedMembersIndex.Count > 0)\n                UpdateMemberZoneCustomProperties(updatedMembersIndex);\n\n            _membersIndex = updatedMembersIndex;\n\n            _dnsServer.AuthZoneManager.SaveZoneFile(_name);\n        }\n\n        protected override async Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList<DnsResourceRecord> historyRecords)\n        {\n            //secondary catalog does not maintain zone history; no need to call base method\n            string version = GetVersion();\n            if ((version is null) || !version.Equals(\"2\", StringComparison.OrdinalIgnoreCase))\n            {\n                _dnsServer.LogManager.Write(\"Failed to provision Secondary Catalog zone '\" + ToString() + \"': catalog version not supported.\");\n                return;\n            }\n\n            bool isAddHistoryRecord = false;\n            bool updateGlobalAllowQueryProperty = false;\n            bool updateGlobalAllowTransferAndTsigKeyNamesProperties = false;\n            Dictionary<string, object> membersToRemove = new Dictionary<string, object>();\n            Dictionary<string, string> membersToAdd = new Dictionary<string, string>();\n            Dictionary<string, string> membersToUpdate = new Dictionary<string, string>();\n\n            //inspect records in history\n            for (int i = 1; i < historyRecords.Count; i++)\n            {\n                DnsResourceRecord historyRecord = historyRecords[i];\n                if (historyRecord.Type == DnsResourceRecordType.SOA)\n                {\n                    isAddHistoryRecord = true; //removed records completed\n                    continue;\n                }\n\n                if (historyRecord.Name.Length == _name.Length)\n                    continue; //skip apex records\n\n                string subdomain = historyRecord.Name.Substring(0, historyRecord.Name.Length - _name.Length - 1).ToLowerInvariant();\n                string[] labels = subdomain.Split('.');\n                Array.Reverse(labels);\n\n                switch (labels[0])\n                {\n                    case \"ext\":\n                        if (labels.Length > 1)\n                        {\n                            switch (labels[1])\n                            {\n                                case \"allow-query\":\n                                    updateGlobalAllowQueryProperty = true;\n                                    break;\n\n                                case \"allow-transfer\":\n                                case \"transfer-tsig-key-names\":\n                                    updateGlobalAllowTransferAndTsigKeyNamesProperties = true;\n                                    break;\n                            }\n                        }\n                        break;\n\n                    case \"zones\":\n                        if (labels.Length == 2)\n                        {\n                            if (historyRecord.Type == DnsResourceRecordType.PTR)\n                            {\n                                string memberZoneName = (historyRecord.RDATA as DnsPTRRecordData).Domain.ToLowerInvariant();\n\n                                if (isAddHistoryRecord)\n                                {\n                                    string memberZoneDomain = subdomain + \".\" + _name;\n\n                                    membersToAdd.TryAdd(memberZoneName, memberZoneDomain);\n                                    membersToUpdate.TryAdd(memberZoneName, memberZoneDomain);\n                                }\n                                else\n                                {\n                                    membersToRemove.TryAdd(memberZoneName, null);\n                                }\n                            }\n                        }\n                        else if (labels.Length > 2)\n                        {\n                            switch (labels[2])\n                            {\n                                case \"ext\":\n                                case \"coo\":\n                                    string memberZoneDomain = labels[1] + \".\" + labels[0] + \".\" + _name;\n                                    DnsResourceRecord prevHistoryRecord = historyRecords[i - 1];\n\n                                    if (prevHistoryRecord.Name.EndsWith(memberZoneDomain, StringComparison.OrdinalIgnoreCase))\n                                        break; //skip since its same member zone's custom property\n\n                                    IReadOnlyList<DnsResourceRecord> ptrRecords = _dnsServer.AuthZoneManager.GetRecords(_name, memberZoneDomain, DnsResourceRecordType.PTR);\n                                    if (ptrRecords.Count > 0)\n                                        membersToUpdate.TryAdd((ptrRecords[0].RDATA as DnsPTRRecordData).Domain.ToLowerInvariant(), memberZoneDomain);\n\n                                    break;\n                            }\n                        }\n\n                        break;\n                }\n            }\n\n            //apply changes\n            if (updateGlobalAllowQueryProperty)\n                UpdateGlobalAllowQueryProperty();\n\n            if (updateGlobalAllowTransferAndTsigKeyNamesProperties)\n                UpdateGlobalAllowTransferAndTsigKeyNamesProperties();\n\n            if ((membersToRemove.Count > 0) || (membersToAdd.Count > 0))\n                await AddAndRemoveMemberZonesAsync(membersToRemove, membersToAdd);\n\n            if (membersToUpdate.Count > 0)\n                UpdateMemberZoneCustomProperties(membersToUpdate);\n\n            if ((membersToRemove.Count > 0) || (membersToAdd.Count > 0))\n            {\n                //update members index\n                Dictionary<string, string> updatedMembersIndex = new Dictionary<string, string>(_membersIndex);\n\n                foreach (KeyValuePair<string, object> removedMember in membersToRemove)\n                    updatedMembersIndex.Remove(removedMember.Key);\n\n                foreach (KeyValuePair<string, string> addedMember in membersToAdd)\n                    updatedMembersIndex.TryAdd(addedMember.Key, addedMember.Value);\n\n                _membersIndex = updatedMembersIndex;\n            }\n\n            _dnsServer.AuthZoneManager.SaveZoneFile(_name);\n        }\n\n        private async Task AddAndRemoveMemberZonesAsync(Dictionary<string, object> membersToRemove, Dictionary<string, string> membersToAdd)\n        {\n            //remove zones\n            foreach (KeyValuePair<string, object> removeMember in membersToRemove)\n            {\n                ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(removeMember.Key);\n                if ((apexZone is not null) && _name.Equals(apexZone.CatalogZoneName, StringComparison.OrdinalIgnoreCase))\n                    DeleteMemberZone(apexZone);\n            }\n\n            //add zones\n            List<Task> tasks = new List<Task>(membersToAdd.Count);\n\n            foreach (KeyValuePair<string, string> addMember in membersToAdd)\n            {\n                string zoneName = addMember.Key;\n                string memberZoneDomain = addMember.Value;\n\n                ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(zoneName);\n                if (apexZone is null)\n                {\n                    //create zone\n                    AuthZoneType zoneType = GetZoneTypeProperty(memberZoneDomain);\n                    switch (zoneType)\n                    {\n                        case AuthZoneType.Primary:\n                            {\n                                TaskCompletionSource taskSource = new TaskCompletionSource();\n\n                                if (_dnsServer.TryQueueResolverTask(async delegate (object state)\n                                {\n                                    //create secondary zone\n                                    try\n                                    {\n                                        IReadOnlyList<NameServerAddress> primaryNameServerAddresses;\n                                        DnsTransportProtocol primaryZoneTransferProtocol;\n                                        string primaryZoneTransferTsigKeyName;\n\n                                        List<Tuple<IPAddress, string>> primaries = GetPrimariesProperty(memberZoneDomain);\n                                        if (primaries.Count == 0)\n                                            primaries = GetPrimariesProperty(_name);\n\n                                        bool overrideCatalogPrimaryNameServers;\n\n                                        if (primaries.Count > 0)\n                                        {\n                                            List<NameServerAddress> primaryNameServerAddressesList = new List<NameServerAddress>();\n\n                                            primaryNameServerAddresses = primaryNameServerAddressesList;\n                                            primaryZoneTransferProtocol = DnsTransportProtocol.Tcp;\n                                            primaryZoneTransferTsigKeyName = primaries[0].Item2;\n                                            overrideCatalogPrimaryNameServers = true;\n\n                                            foreach (Tuple<IPAddress, string> primaryNameServer in primaries)\n                                            {\n                                                if (primaryNameServer.Item2 == primaryZoneTransferTsigKeyName)\n                                                    primaryNameServerAddressesList.Add(new NameServerAddress(primaryNameServer.Item1, DnsTransportProtocol.Tcp));\n                                            }\n                                        }\n                                        else\n                                        {\n                                            overrideCatalogPrimaryNameServers = false;\n                                            primaryNameServerAddresses = PrimaryNameServerAddresses;\n                                            primaryZoneTransferProtocol = PrimaryZoneTransferProtocol;\n                                            primaryZoneTransferTsigKeyName = PrimaryZoneTransferTsigKeyName;\n                                        }\n\n                                        AuthZoneInfo zoneInfo = await _dnsServer.AuthZoneManager.CreateSecondaryZoneAsync(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, false, true);\n\n                                        zoneInfo.OverrideCatalogPrimaryNameServers = overrideCatalogPrimaryNameServers;\n\n                                        //set as catalog zone member\n                                        zoneInfo.ApexZone.CatalogZoneName = _name;\n\n                                        //raise event\n                                        ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo));\n\n                                        //write log\n                                        _dnsServer.LogManager.Write(zoneInfo.TypeName + \" zone '\" + zoneInfo.DisplayName + \"' was added via Secondary Catalog zone '\" + ToString() + \"' sucessfully.\");\n                                    }\n                                    catch (Exception ex)\n                                    {\n                                        _dnsServer.LogManager.Write(ex);\n                                    }\n                                    finally\n                                    {\n                                        (state as TaskCompletionSource).TrySetResult();\n                                    }\n                                }, taskSource))\n                                {\n                                    tasks.Add(taskSource.Task);\n                                }\n                            }\n                            break;\n\n                        case AuthZoneType.Secondary:\n                            {\n                                TaskCompletionSource taskSource = new TaskCompletionSource();\n\n                                if (_dnsServer.TryQueueResolverTask(async delegate (object state)\n                                {\n                                    //create secondary zone\n                                    try\n                                    {\n                                        IReadOnlyList<NameServerAddress> primaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain);\n                                        DnsTransportProtocol primaryZoneTransferProtocol = GetPrimaryZoneTransferProtocolProperty(memberZoneDomain);\n                                        string primaryZoneTransferTsigKeyName = GetPrimaryZoneTransferTsigKeyNameProperty(memberZoneDomain);\n                                        bool validateZone = GetZoneMdValidationProperty(memberZoneDomain);\n\n                                        AuthZoneInfo zoneInfo = await _dnsServer.AuthZoneManager.CreateSecondaryZoneAsync(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, true);\n\n                                        zoneInfo.OverrideCatalogPrimaryNameServers = true; //always true for secondary member zones\n\n                                        //set as catalog zone member\n                                        zoneInfo.ApexZone.CatalogZoneName = _name;\n\n                                        //raise event\n                                        ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo));\n\n                                        //write log\n                                        _dnsServer.LogManager.Write(zoneInfo.TypeName + \" zone '\" + zoneInfo.DisplayName + \"' was added via Secondary Catalog zone '\" + ToString() + \"' sucessfully.\");\n                                    }\n                                    catch (Exception ex)\n                                    {\n                                        _dnsServer.LogManager.Write(ex);\n                                    }\n                                    finally\n                                    {\n                                        (state as TaskCompletionSource).TrySetResult();\n                                    }\n                                }, taskSource))\n                                {\n                                    tasks.Add(taskSource.Task);\n                                }\n                            }\n                            break;\n\n                        case AuthZoneType.Stub:\n                            {\n                                TaskCompletionSource taskSource = new TaskCompletionSource();\n\n                                if (_dnsServer.TryQueueResolverTask(async delegate (object state)\n                                {\n                                    //create stub zone\n                                    try\n                                    {\n                                        IReadOnlyList<NameServerAddress> primaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain);\n\n                                        AuthZoneInfo zoneInfo = await _dnsServer.AuthZoneManager.CreateStubZoneAsync(zoneName, primaryNameServerAddresses, true);\n\n                                        //set as catalog zone member\n                                        zoneInfo.ApexZone.CatalogZoneName = _name;\n\n                                        //raise event\n                                        ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo));\n\n                                        //write log\n                                        _dnsServer.LogManager.Write(zoneInfo.TypeName + \" zone '\" + zoneInfo.DisplayName + \"' was added via Secondary Catalog zone '\" + ToString() + \"' sucessfully.\");\n                                    }\n                                    catch (Exception ex)\n                                    {\n                                        _dnsServer.LogManager.Write(ex);\n                                    }\n                                    finally\n                                    {\n                                        (state as TaskCompletionSource).TrySetResult();\n                                    }\n                                }, taskSource))\n                                {\n                                    tasks.Add(taskSource.Task);\n                                }\n                            }\n                            break;\n\n                        case AuthZoneType.Forwarder:\n                            {\n                                //create secondary forwarder zone\n                                try\n                                {\n                                    AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.CreateSecondaryForwarderZone(zoneName, PrimaryNameServerAddresses, PrimaryZoneTransferProtocol, PrimaryZoneTransferTsigKeyName);\n\n                                    //set as catalog zone member\n                                    zoneInfo.ApexZone.CatalogZoneName = _name;\n\n                                    //raise event\n                                    ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo));\n\n                                    //write log\n                                    _dnsServer.LogManager.Write(zoneInfo.TypeName + \" zone '\" + zoneInfo.DisplayName + \"' was added via Secondary Catalog zone '\" + ToString() + \"' sucessfully.\");\n                                }\n                                catch (Exception ex)\n                                {\n                                    _dnsServer.LogManager.Write(ex);\n                                }\n                            }\n                            break;\n                    }\n                }\n            }\n\n            //wait for all zones to be added\n            foreach (Task task in tasks)\n                await task;\n        }\n\n        private void UpdateGlobalAllowQueryProperty()\n        {\n            //allow query global custom property\n            IReadOnlyCollection<NetworkAccessControl> globalAllowQueryACL = GetAllowQueryProperty(_name);\n            if (globalAllowQueryACL.Count > 0)\n            {\n                _queryAccess = GetQueryAccessType(globalAllowQueryACL);\n                switch (_queryAccess)\n                {\n                    case AuthZoneQueryAccess.UseSpecifiedNetworkACL:\n                        QueryAccessNetworkACL = globalAllowQueryACL;\n                        break;\n\n                    case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        QueryAccessNetworkACL = GetFilteredACL(globalAllowQueryACL);\n                        break;\n\n                    default:\n                        QueryAccessNetworkACL = null;\n                        break;\n                }\n            }\n            else\n            {\n                _queryAccess = AuthZoneQueryAccess.Allow;\n                QueryAccessNetworkACL = null;\n            }\n        }\n\n        private void UpdateGlobalAllowTransferAndTsigKeyNamesProperties()\n        {\n            //allow transfer global custom property\n            IReadOnlyCollection<NetworkAccessControl> globalAllowTransferACL = GetAllowTransferProperty(_name);\n            if (globalAllowTransferACL.Count > 0)\n            {\n                _zoneTransfer = GetZoneTransferType(globalAllowTransferACL);\n                switch (_zoneTransfer)\n                {\n                    case AuthZoneTransfer.UseSpecifiedNetworkACL:\n                        ZoneTransferNetworkACL = globalAllowTransferACL;\n                        break;\n\n                    case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        ZoneTransferNetworkACL = GetFilteredACL(globalAllowTransferACL);\n                        break;\n\n                    default:\n                        ZoneTransferNetworkACL = null;\n                        break;\n                }\n\n                //zone tranfer tsig key names global custom property\n                ZoneTransferTsigKeyNames = GetZoneTransferTsigKeyNamesProperty(_name);\n            }\n            else\n            {\n                _zoneTransfer = AuthZoneTransfer.Deny;\n                ZoneTransferNetworkACL = null;\n                ZoneTransferTsigKeyNames = null;\n            }\n        }\n\n        private void UpdateMemberZoneCustomProperties(Dictionary<string, string> membersToUpdate)\n        {\n            foreach (KeyValuePair<string, string> updatedMemberEntry in membersToUpdate)\n            {\n                string zoneName = updatedMemberEntry.Key;\n                string memberZoneDomain = updatedMemberEntry.Value;\n\n                ApexZone memberApexZone = _dnsServer.AuthZoneManager.GetApexZone(zoneName);\n                if ((memberApexZone is not null) && _name.Equals(memberApexZone.CatalogZoneName, StringComparison.OrdinalIgnoreCase))\n                {\n                    //change of ownership property\n                    {\n                        string newCatalogZoneName = GetChangeOfOwnershipProperty(memberZoneDomain);\n                        if (newCatalogZoneName is not null)\n                        {\n                            ApexZone catalogApexZone = _dnsServer.AuthZoneManager.GetApexZone(newCatalogZoneName);\n                            if (catalogApexZone is SecondaryCatalogZone secondaryCatalogZone)\n                            {\n                                //found secondary catalog zone; transfer ownership to it\n                                memberApexZone.CatalogZoneName = secondaryCatalogZone._name;\n                            }\n                            else\n                            {\n                                //no such secondary catalog zone exists; delete member zone\n                                DeleteMemberZone(memberApexZone);\n                                continue;\n                            }\n                        }\n                    }\n\n                    //allow query member zone custom property\n                    {\n                        IReadOnlyCollection<NetworkAccessControl> allowQueryACL = GetAllowQueryProperty(memberZoneDomain);\n                        if (allowQueryACL.Count > 0)\n                        {\n                            memberApexZone.QueryAccess = GetQueryAccessType(allowQueryACL);\n\n                            switch (memberApexZone.QueryAccess)\n                            {\n                                case AuthZoneQueryAccess.UseSpecifiedNetworkACL:\n                                    memberApexZone.QueryAccessNetworkACL = allowQueryACL;\n                                    break;\n\n                                case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                                    memberApexZone.QueryAccessNetworkACL = GetFilteredACL(allowQueryACL);\n                                    break;\n\n                                default:\n                                    memberApexZone.QueryAccessNetworkACL = null;\n                                    break;\n                            }\n\n                            memberApexZone.OverrideCatalogQueryAccess = true;\n                        }\n                        else\n                        {\n                            memberApexZone.OverrideCatalogQueryAccess = false;\n                            memberApexZone.QueryAccess = AuthZoneQueryAccess.Allow;\n                            memberApexZone.QueryAccessNetworkACL = null;\n                        }\n                    }\n\n                    if (memberApexZone is StubZone stubZone)\n                    {\n                        //primary addresses property\n                        stubZone.PrimaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain);\n                    }\n                    else if (memberApexZone is SecondaryForwarderZone)\n                    {\n                        //do nothing\n                    }\n                    else if (memberApexZone is SecondaryZone secondaryZone)\n                    {\n                        AuthZoneType zoneType = GetZoneTypeProperty(memberZoneDomain);\n                        if (zoneType == AuthZoneType.Secondary)\n                        {\n                            secondaryZone.PrimaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain);\n                            secondaryZone.PrimaryZoneTransferProtocol = GetPrimaryZoneTransferProtocolProperty(memberZoneDomain);\n                            secondaryZone.PrimaryZoneTransferTsigKeyName = GetPrimaryZoneTransferTsigKeyNameProperty(memberZoneDomain);\n                            secondaryZone.ValidateZone = GetZoneMdValidationProperty(memberZoneDomain);\n                        }\n                        else\n                        {\n                            //primaries property\n                            List<Tuple<IPAddress, string>> primaries = GetPrimariesProperty(memberZoneDomain);\n                            if (primaries.Count == 0)\n                                primaries = GetPrimariesProperty(_name);\n\n                            if (primaries.Count > 0)\n                            {\n                                List<NameServerAddress> primaryNameServerAddresses = new List<NameServerAddress>();\n                                string primaryZoneTransferTsigKeyName = primaries[0].Item2;\n\n                                foreach (Tuple<IPAddress, string> primaryNameServer in primaries)\n                                {\n                                    if (primaryNameServer.Item2 == primaryZoneTransferTsigKeyName)\n                                        primaryNameServerAddresses.Add(new NameServerAddress(primaryNameServer.Item1, DnsTransportProtocol.Tcp));\n                                }\n\n                                secondaryZone.PrimaryNameServerAddresses = primaryNameServerAddresses;\n                                secondaryZone.PrimaryZoneTransferProtocol = DnsTransportProtocol.Tcp;\n                                secondaryZone.PrimaryZoneTransferTsigKeyName = primaryZoneTransferTsigKeyName;\n                                secondaryZone.OverrideCatalogPrimaryNameServers = true;\n                            }\n                            else\n                            {\n                                secondaryZone.OverrideCatalogPrimaryNameServers = false;\n                                secondaryZone.PrimaryNameServerAddresses = null;\n                                secondaryZone.PrimaryZoneTransferProtocol = DnsTransportProtocol.Tcp;\n                                secondaryZone.PrimaryZoneTransferTsigKeyName = null;\n                            }\n                        }\n\n                        //allow transfer member zone custom property\n                        IReadOnlyCollection<NetworkAccessControl> allowTransferACL = GetAllowTransferProperty(memberZoneDomain);\n                        if (allowTransferACL.Count > 0)\n                        {\n                            memberApexZone.ZoneTransfer = GetZoneTransferType(allowTransferACL);\n\n                            switch (memberApexZone.ZoneTransfer)\n                            {\n                                case AuthZoneTransfer.UseSpecifiedNetworkACL:\n                                    memberApexZone.ZoneTransferNetworkACL = allowTransferACL;\n                                    break;\n\n                                case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                                    memberApexZone.ZoneTransferNetworkACL = GetFilteredACL(allowTransferACL);\n                                    break;\n\n                                default:\n                                    memberApexZone.ZoneTransferNetworkACL = null;\n                                    break;\n                            }\n\n                            //zone tranfer tsig key names member zone custom property\n                            memberApexZone.ZoneTransferTsigKeyNames = GetZoneTransferTsigKeyNamesProperty(memberZoneDomain);\n\n                            memberApexZone.OverrideCatalogZoneTransfer = true;\n                        }\n                        else\n                        {\n                            memberApexZone.OverrideCatalogZoneTransfer = false;\n                            memberApexZone.ZoneTransfer = AuthZoneTransfer.Deny;\n                            memberApexZone.ZoneTransferNetworkACL = null;\n                            memberApexZone.ZoneTransferTsigKeyNames = null;\n                        }\n                    }\n\n                    _dnsServer.AuthZoneManager.SaveZoneFile(memberApexZone.Name);\n                }\n            }\n        }\n\n        private void DeleteMemberZone(ApexZone apexZone)\n        {\n            AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone);\n\n            if (_dnsServer.AuthZoneManager.DeleteZone(zoneInfo, true))\n            {\n                ZoneRemoved?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo));\n\n                _dnsServer.LogManager.Write(apexZone.GetZoneTypeName() + \" zone '\" + apexZone.ToString() + \"' was removed via Secondary Catalog zone '\" + ToString() + \"' sucessfully.\");\n            }\n        }\n\n        private string GetVersion()\n        {\n            string domain = \"version.\" + _name;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT);\n            if (records.Count > 0)\n                return (records[0].RDATA as DnsTXTRecordData).GetText();\n\n            return null;\n\n        }\n\n        private string GetChangeOfOwnershipProperty(string memberZoneDomain)\n        {\n            string domain = \"coo.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.PTR);\n            if (records.Count > 0)\n                return (records[0].RDATA as DnsPTRRecordData).Domain;\n\n            return null;\n        }\n\n        private AuthZoneType GetZoneTypeProperty(string memberZoneDomain)\n        {\n            string domain = \"zone-type.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT);\n            if (records.Count > 0)\n                return Enum.Parse<AuthZoneType>((records[0].RDATA as DnsTXTRecordData).GetText(), true);\n\n            return AuthZoneType.Primary;\n        }\n\n        private List<Tuple<IPAddress, string>> GetPrimariesProperty(string memberZoneDomain)\n        {\n            string domain = \"primaries.ext.\" + memberZoneDomain;\n\n            List<Tuple<IPAddress, string>> primaries = new List<Tuple<IPAddress, string>>(2);\n\n            AuthZone authZone = _dnsServer.AuthZoneManager.GetAuthZone(_name, domain);\n            if (authZone is not null)\n            {\n                foreach (DnsResourceRecord record in authZone.GetRecords(DnsResourceRecordType.A))\n                    primaries.Add(new Tuple<IPAddress, string>((record.RDATA as DnsARecordData).Address, null));\n\n                foreach (DnsResourceRecord record in authZone.GetRecords(DnsResourceRecordType.AAAA))\n                    primaries.Add(new Tuple<IPAddress, string>((record.RDATA as DnsAAAARecordData).Address, null));\n            }\n\n            List<string> subdomains = new List<string>();\n            _dnsServer.AuthZoneManager.ListSubDomains(domain, subdomains);\n\n            foreach (string subdomain in subdomains)\n            {\n                AuthZone subZone = _dnsServer.AuthZoneManager.GetAuthZone(_name, subdomain + \".\" + domain);\n                if (subZone is null)\n                    continue;\n\n                string tsigKeyName = null;\n                IReadOnlyList<DnsResourceRecord> szTXTRecords = subZone.GetRecords(DnsResourceRecordType.TXT);\n                if (szTXTRecords.Count > 0)\n                    tsigKeyName = (szTXTRecords[0].RDATA as DnsTXTRecordData).GetText();\n\n                foreach (DnsResourceRecord record in subZone.GetRecords(DnsResourceRecordType.A))\n                    primaries.Add(new Tuple<IPAddress, string>((record.RDATA as DnsARecordData).Address, tsigKeyName));\n\n                foreach (DnsResourceRecord record in subZone.GetRecords(DnsResourceRecordType.AAAA))\n                    primaries.Add(new Tuple<IPAddress, string>((record.RDATA as DnsAAAARecordData).Address, tsigKeyName));\n            }\n\n            return primaries;\n        }\n\n        private IReadOnlyList<NameServerAddress> GetPrimaryAddressesProperty(string memberZoneDomain)\n        {\n            string domain = \"primary-addresses.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT);\n            if (records.Count > 0)\n                return (records[0].RDATA as DnsTXTRecordData).CharacterStrings.Convert(NameServerAddress.Parse);\n\n            return [];\n        }\n\n        private DnsTransportProtocol GetPrimaryZoneTransferProtocolProperty(string memberZoneDomain)\n        {\n            string domain = \"primary-transfer-protocol.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT);\n            if (records.Count > 0)\n                return Enum.Parse<DnsTransportProtocol>((records[0].RDATA as DnsTXTRecordData).CharacterStrings[0], true);\n\n            return DnsTransportProtocol.Tcp;\n        }\n\n        private string GetPrimaryZoneTransferTsigKeyNameProperty(string memberZoneDomain)\n        {\n            string domain = \"primary-transfer-tsig-key-name.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.PTR);\n            if (records.Count > 0)\n                return (records[0].RDATA as DnsPTRRecordData).Domain;\n\n            return null;\n        }\n\n        private bool GetZoneMdValidationProperty(string memberZoneDomain)\n        {\n            string domain = \"zonemd-validation.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT);\n            if (records.Count > 0)\n                return bool.Parse((records[0].RDATA as DnsTXTRecordData).CharacterStrings[0]);\n\n            return false;\n        }\n\n        private IReadOnlyCollection<NetworkAccessControl> GetAllowQueryProperty(string memberZoneDomain)\n        {\n            string domain = \"allow-query.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.APL);\n            if (records.Count > 0)\n                return NetworkAccessControl.ConvertFromAPLRecordData(records[0].RDATA as DnsAPLRecordData);\n\n            return [];\n        }\n\n        private IReadOnlyCollection<NetworkAccessControl> GetAllowTransferProperty(string memberZoneDomain)\n        {\n            string domain = \"allow-transfer.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.APL);\n            if (records.Count > 0)\n                return NetworkAccessControl.ConvertFromAPLRecordData(records[0].RDATA as DnsAPLRecordData);\n\n            return [];\n        }\n\n        private HashSet<string> GetZoneTransferTsigKeyNamesProperty(string memberZoneDomain)\n        {\n            string domain = \"transfer-tsig-key-names.ext.\" + memberZoneDomain;\n\n            IReadOnlyList<DnsResourceRecord> records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.PTR);\n            HashSet<string> keyNames = new HashSet<string>(records.Count);\n\n            foreach (DnsResourceRecord record in records)\n                keyNames.Add((record.RDATA as DnsPTRRecordData).Domain.ToLowerInvariant());\n\n            return keyNames;\n        }\n\n        private static AuthZoneQueryAccess GetQueryAccessType(IReadOnlyCollection<NetworkAccessControl> acl)\n        {\n            if (acl.HasSameItems(_allowACL))\n                return AuthZoneQueryAccess.Allow;\n\n            if (acl.HasSameItems(_queryAccessAllowOnlyPrivateNetworksACL))\n                return AuthZoneQueryAccess.AllowOnlyPrivateNetworks;\n\n            if (acl.HasSameItems(_allowOnlyZoneNameServersACL))\n                return AuthZoneQueryAccess.AllowOnlyZoneNameServers;\n\n            if ((acl.Count > 1) && acl.Contains(_allowZoneNameServersAndUseSpecifiedNetworkACL))\n                return AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL;\n\n            if (acl.HasSameItems(_denyACL))\n                return AuthZoneQueryAccess.Deny;\n\n            return AuthZoneQueryAccess.UseSpecifiedNetworkACL;\n        }\n\n        private static AuthZoneTransfer GetZoneTransferType(IReadOnlyCollection<NetworkAccessControl> acl)\n        {\n            if (acl.HasSameItems(_allowACL))\n                return AuthZoneTransfer.Allow;\n\n            if (acl.HasSameItems(_allowOnlyZoneNameServersACL))\n                return AuthZoneTransfer.AllowOnlyZoneNameServers;\n\n            if ((acl.Count > 1) && acl.Contains(_allowZoneNameServersAndUseSpecifiedNetworkACL))\n                return AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL;\n\n            if (acl.HasSameItems(_denyACL))\n                return AuthZoneTransfer.Deny;\n\n            return AuthZoneTransfer.UseSpecifiedNetworkACL;\n        }\n\n        private static List<NetworkAccessControl> GetFilteredACL(IReadOnlyCollection<NetworkAccessControl> acl)\n        {\n            List<NetworkAccessControl> filteredACL = new List<NetworkAccessControl>(acl.Count);\n\n            foreach (NetworkAccessControl ac in acl)\n            {\n                if (ac.Equals(_allowZoneNameServersAndUseSpecifiedNetworkACL))\n                    continue;\n\n                filteredACL.Add(ac);\n            }\n\n            return filteredACL;\n        }\n\n        #endregion\n\n        #region public\n\n        public override string GetZoneTypeName()\n        {\n            return \"Secondary Catalog\";\n        }\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            return []; //secondary catalog zone is not queriable\n        }\n\n        #endregion\n\n        #region properties\n\n        public override string CatalogZoneName\n        {\n            get { return base.CatalogZoneName; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override bool OverrideCatalogQueryAccess\n        {\n            get { throw new InvalidOperationException(); }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneQueryAccess QueryAccess\n        {\n            get { return _queryAccess; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneTransfer ZoneTransfer\n        {\n            get { return _zoneTransfer; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneUpdate Update\n        {\n            get { return base.Update; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        #endregion\n    }\n\n    public class SecondaryCatalogEventArgs : EventArgs\n    {\n        #region variables\n\n        readonly AuthZoneInfo _zoneInfo;\n\n        #endregion\n\n        #region constructor\n\n        public SecondaryCatalogEventArgs(AuthZoneInfo zoneInfo)\n        {\n            _zoneInfo = zoneInfo;\n        }\n\n        #endregion\n\n        #region properties\n\n        public AuthZoneInfo ZoneInfo\n        { get { return _zoneInfo; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/SecondaryForwarderZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class SecondaryForwarderZone : SecondaryZone\n    {\n        #region constructor\n\n        public SecondaryForwarderZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(dnsServer, zoneInfo)\n        { }\n\n        public SecondaryForwarderZone(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null)\n            : base(dnsServer, name, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, false)\n        {\n            InitZone();\n        }\n\n        #endregion\n\n        #region protected\n\n        protected virtual void InitZone()\n        {\n            //init secondary forwarder zone with dummy SOA record\n            DnsSOARecordData soa = new DnsSOARecordData(_dnsServer.ServerDomain, \"invalid\", 0, 900, 300, 604800, 900);\n            DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);\n            soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n            _entries[DnsResourceRecordType.SOA] = [soaRecord];\n        }\n\n        protected override Task FinalizeZoneTransferAsync()\n        {\n            //secondary forwarder does not maintain zone history; no need to call base method\n            return Task.CompletedTask;\n        }\n\n        protected override Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList<DnsResourceRecord> historyRecords)\n        {\n            //secondary forwarder does not maintain zone history; no need to call base method\n            return Task.CompletedTask;\n        }\n\n        #endregion\n\n        #region public\n\n        public override string GetZoneTypeName()\n        {\n            return \"Secondary Forwarder\";\n        }\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            if (type == DnsResourceRecordType.SOA)\n                return []; //secondary forwarder zone is not authoritative and contains dummy SOA record\n\n            return base.QueryRecords(type, dnssecOk);\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool OverrideCatalogZoneTransfer\n        {\n            get { throw new InvalidOperationException(); }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override bool OverrideCatalogPrimaryNameServers\n        {\n            get { throw new InvalidOperationException(); }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneQueryAccess QueryAccess\n        {\n            get { return base.QueryAccess; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneQueryAccess.AllowOnlyZoneNameServers:\n                    case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        throw new ArgumentException(\"The Query Access option is invalid for Secondary Conditional Forwarder zones: \" + value.ToString(), nameof(QueryAccess));\n                }\n\n                base.QueryAccess = value;\n            }\n        }\n\n        public override AuthZoneTransfer ZoneTransfer\n        {\n            get { return base.ZoneTransfer; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneNotify Notify\n        {\n            get { return base.Notify; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneUpdate Update\n        {\n            get { return base.Update; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneUpdate.AllowOnlyZoneNameServers:\n                    case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        throw new ArgumentException(\"The Dynamic Updates option is invalid for Secondary Conditional Forwarder zones: \" + value.ToString(), nameof(Update));\n                }\n\n                base.Update = value;\n            }\n        }\n\n        public override IReadOnlyList<NameServerAddress> PrimaryNameServerAddresses\n        {\n            get { return base.PrimaryNameServerAddresses; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    throw new ArgumentException(\"At least one primary name server address must be specified for \" + GetZoneTypeName() + \" zone.\", nameof(PrimaryNameServerAddresses));\n\n                base.PrimaryNameServerAddresses = value;\n            }\n        }\n\n        public override bool ValidateZone\n        {\n            get { return base.ValidateZone; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/SecondarySubDomainZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class SecondarySubDomainZone : SubDomainZone\n    {\n        #region variables\n\n        readonly SecondaryZone _secondaryZone;\n\n        #endregion\n\n        #region constructor\n\n        public SecondarySubDomainZone(SecondaryZone secondaryZone, string name)\n            : base(secondaryZone, name)\n        {\n            _secondaryZone = secondaryZone;\n        }\n\n        #endregion\n\n        #region public\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            throw new InvalidOperationException(\"Cannot set records in \" + _secondaryZone.GetZoneTypeName() + \" zone.\");\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            throw new InvalidOperationException(\"Cannot add record in \" + _secondaryZone.GetZoneTypeName() + \" zone.\");\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)\n        {\n            throw new InvalidOperationException(\"Cannot delete record in \" + _secondaryZone.GetZoneTypeName() + \" zone.\");\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            throw new InvalidOperationException(\"Cannot delete records in \" + _secondaryZone.GetZoneTypeName() + \" zone.\");\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            throw new InvalidOperationException(\"Cannot update record in \" + _secondaryZone.GetZoneTypeName() + \" zone.\");\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/SecondaryZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.Dnssec;\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Security.Cryptography;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    //Message Digest for DNS Zones\n    //https://datatracker.ietf.org/doc/rfc8976/\n\n    class SecondaryZone : ApexZone\n    {\n        #region variables\n\n        IReadOnlyCollection<DnssecPrivateKey> _dnssecPrivateKeys; //for holding DNSSEC private keys as a backup on secondary cluster nodes\n\n        readonly object _refreshTimerLock = new object();\n        Timer _refreshTimer;\n        bool _refreshTimerTriggered;\n        const int REFRESH_TIMER_INTERVAL = 5000;\n\n        const int REFRESH_SOA_TIMEOUT = 10000;\n        const int REFRESH_XFR_TIMEOUT = 120000;\n        const int REFRESH_RETRIES = 5;\n\n        const int REFRESH_TSIG_FUDGE = 300;\n\n        bool _overrideCatalogPrimaryNameServers;\n\n        IReadOnlyList<NameServerAddress> _primaryNameServerAddresses;\n        DnsTransportProtocol _primaryZoneTransferProtocol;\n        string _primaryZoneTransferTsigKeyName;\n\n        DateTime _expiry;\n        bool _isExpired;\n\n        bool _validateZone;\n        bool _validationFailed;\n\n        bool _resync;\n\n        #endregion\n\n        #region constructor\n\n        public SecondaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(dnsServer, zoneInfo)\n        {\n            _dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys;\n\n            _overrideCatalogPrimaryNameServers = zoneInfo.OverrideCatalogPrimaryNameServers;\n\n            _primaryNameServerAddresses = zoneInfo.PrimaryNameServerAddresses;\n            _primaryZoneTransferProtocol = zoneInfo.PrimaryZoneTransferProtocol;\n            _primaryZoneTransferTsigKeyName = zoneInfo.PrimaryZoneTransferTsigKeyName;\n\n            _expiry = zoneInfo.Expiry;\n            _isExpired = DateTime.UtcNow > _expiry;\n\n            _validateZone = zoneInfo.ValidateZone;\n            _validationFailed = zoneInfo.ValidationFailed;\n\n            _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n\n            InitNotify();\n        }\n\n        protected SecondaryZone(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol, string primaryZoneTransferTsigKeyName, bool validateZone)\n            : base(dnsServer, name)\n        {\n            PrimaryZoneTransferProtocol = primaryZoneTransferProtocol;\n\n            PrimaryNameServerAddresses = primaryNameServerAddresses?.Convert(delegate (NameServerAddress nameServer)\n            {\n                if (nameServer.Protocol != primaryZoneTransferProtocol)\n                    nameServer = nameServer.Clone(primaryZoneTransferProtocol);\n\n                return nameServer;\n            });\n\n            PrimaryZoneTransferTsigKeyName = primaryZoneTransferTsigKeyName;\n            _validateZone = validateZone;\n\n            _isExpired = true; //new secondary zone is considered expired till it refreshes\n\n            _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n\n            InitNotify();\n        }\n\n        #endregion\n\n        #region static\n\n        public static async Task<SecondaryZone> CreateAsync(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false)\n        {\n            SecondaryZone secondaryZone = new SecondaryZone(dnsServer, name, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone);\n\n            try\n            {\n                DnsDatagram soaResponse;\n\n                DnsQuestionRecord soaQuestion = new DnsQuestionRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN);\n\n                if (secondaryZone.PrimaryNameServerAddresses is null)\n                {\n                    soaResponse = await secondaryZone._dnsServer.DirectQueryAsync(soaQuestion);\n                }\n                else\n                {\n                    DnsClient dnsClient = new DnsClient(secondaryZone.PrimaryNameServerAddresses);\n                    List<Task> tasks = new List<Task>(dnsClient.Servers.Count);\n\n                    foreach (NameServerAddress nameServerAddress in dnsClient.Servers)\n                    {\n                        if (nameServerAddress.IsIPEndPointStale)\n                            tasks.Add(nameServerAddress.ResolveIPAddressAsync(secondaryZone._dnsServer, secondaryZone._dnsServer.PreferIPv6));\n                    }\n\n                    await Task.WhenAll(tasks);\n\n                    dnsClient.Proxy = secondaryZone._dnsServer.Proxy;\n                    dnsClient.PreferIPv6 = secondaryZone._dnsServer.PreferIPv6;\n\n                    DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [soaQuestion], null, null, null, secondaryZone._dnsServer.UdpPayloadSize);\n\n                    if (string.IsNullOrEmpty(primaryZoneTransferTsigKeyName))\n                        soaResponse = await dnsClient.RawResolveAsync(soaRequest);\n                    else if ((secondaryZone._dnsServer.TsigKeys is not null) && secondaryZone._dnsServer.TsigKeys.TryGetValue(primaryZoneTransferTsigKeyName, out TsigKey key))\n                        soaResponse = await dnsClient.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);\n                    else\n                        throw new DnsServerException(\"No such TSIG key was found configured: \" + primaryZoneTransferTsigKeyName);\n                }\n\n                if ((soaResponse.Answer.Count == 0) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA))\n                    throw new DnsServerException(\"DNS Server did not receive SOA record in response from any of the primary name servers for: \" + secondaryZone.ToString());\n\n                DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];\n                DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;\n\n                DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum);\n                DnsResourceRecord soaRecord = new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, receivedSoaRecord.OriginalTtlValue, soa);\n\n                secondaryZone._entries[DnsResourceRecordType.SOA] = [soaRecord];\n            }\n            catch\n            {\n                if (!ignoreSoaFailure)\n                    throw;\n\n                //continue with dummy SOA\n                DnsSOARecordData soa = new DnsSOARecordData(secondaryZone._dnsServer.ServerDomain, \"invalid\", 0, 300, 60, 604800, 900);\n                DnsResourceRecord soaRecord = new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);\n                soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                secondaryZone._entries[DnsResourceRecordType.SOA] = [soaRecord];\n            }\n\n            return secondaryZone;\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected override void Dispose(bool disposing)\n        {\n            try\n            {\n                if (_disposed)\n                    return;\n\n                if (disposing)\n                {\n                    lock (_refreshTimerLock)\n                    {\n                        if (_refreshTimer != null)\n                        {\n                            _refreshTimer.Dispose();\n                            _refreshTimer = null;\n                        }\n                    }\n                }\n\n                _disposed = true;\n            }\n            finally\n            {\n                base.Dispose(disposing);\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private void RefreshTimerCallback(object state)\n        {\n            //refresh zone in DNS server's resolver thread pool\n            if (!_dnsServer.TryQueueResolverTask(async delegate (object state)\n                {\n                    try\n                    {\n                        if (Disabled && !_resync)\n                            return;\n\n                        _isExpired = DateTime.UtcNow > _expiry;\n\n                        //get primary name server addresses\n                        IReadOnlyList<NameServerAddress> primaryNameServerAddresses;\n                        DnsTransportProtocol primaryZoneTransferProtocol;\n                        string primaryZoneTransferTsigKeyName;\n\n                        SecondaryCatalogZone secondaryCatalogZone = SecondaryCatalogZone;\n\n                        if ((secondaryCatalogZone is not null) && !_overrideCatalogPrimaryNameServers)\n                        {\n                            primaryNameServerAddresses = await GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses);\n                            primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol;\n                            primaryZoneTransferTsigKeyName = secondaryCatalogZone.PrimaryZoneTransferTsigKeyName;\n                        }\n                        else\n                        {\n                            primaryNameServerAddresses = await GetResolvedPrimaryNameServerAddressesAsync();\n                            primaryZoneTransferProtocol = _primaryZoneTransferProtocol;\n                            primaryZoneTransferTsigKeyName = _primaryZoneTransferTsigKeyName;\n                        }\n\n                        DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];\n                        DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;\n\n                        if (primaryNameServerAddresses.Count == 0)\n                        {\n                            _dnsServer.LogManager.Write(\"DNS Server could not find primary name server IP addresses for \" + GetZoneTypeName() + \" zone: \" + ToString());\n\n                            //set timer for retry\n                            ResetRefreshTimer(Math.Max(currentSoa.Retry, _dnsServer.AuthZoneManager.MinSoaRetry) * 1000);\n                            _syncFailed = true;\n                            return;\n                        }\n\n                        TsigKey key = null;\n\n                        if (!string.IsNullOrEmpty(primaryZoneTransferTsigKeyName) && ((_dnsServer.TsigKeys is null) || !_dnsServer.TsigKeys.TryGetValue(primaryZoneTransferTsigKeyName, out key)))\n                        {\n                            _dnsServer.LogManager.Write(\"DNS Server does not have TSIG key '\" + primaryZoneTransferTsigKeyName + \"' configured for refreshing \" + GetZoneTypeName() + \" zone: \" + ToString());\n\n                            //set timer for retry\n                            ResetRefreshTimer(Math.Max(currentSoa.Retry, _dnsServer.AuthZoneManager.MinSoaRetry) * 1000);\n                            _syncFailed = true;\n                            return;\n                        }\n\n                        //refresh zone\n                        if (await RefreshZoneAsync(primaryNameServerAddresses, primaryZoneTransferProtocol, key, _validateZone))\n                        {\n                            DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;\n\n                            _syncFailed = false;\n                            _expiry = DateTime.UtcNow.AddSeconds(latestSoa.Expire);\n                            _isExpired = false;\n                            _resync = false;\n                            _dnsServer.AuthZoneManager.SaveZoneFile(_name);\n\n                            if (_validationFailed)\n                                ResetRefreshTimer(Math.Max(latestSoa.Retry, _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); //zone validation failed, set timer for retry\n                            else\n                                ResetRefreshTimer(Math.Max(latestSoa.Refresh, _dnsServer.AuthZoneManager.MinSoaRefresh) * 1000); //zone refreshed; set timer for refresh\n\n                            return;\n                        }\n\n                        //no response from any of the name servers; set timer for retry\n                        ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000);\n                        _syncFailed = true;\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsServer.LogManager.Write(ex);\n\n                        //set timer for retry\n                        ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000);\n                        _syncFailed = true;\n                    }\n                    finally\n                    {\n                        _refreshTimerTriggered = false;\n                    }\n                })\n            )\n            {\n                //failed to queue refresh zone task; try again in some time\n                lock (_refreshTimerLock)\n                {\n                    _refreshTimer?.Change(REFRESH_TIMER_INTERVAL, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void ResetRefreshTimer(long dueTime)\n        {\n            lock (_refreshTimerLock)\n            {\n                _refreshTimer?.Change(dueTime, Timeout.Infinite);\n            }\n        }\n\n        private async Task<bool> RefreshZoneAsync(IReadOnlyList<NameServerAddress> primaryNameServers, DnsTransportProtocol zoneTransferProtocol, TsigKey key, bool validateZone)\n        {\n            try\n            {\n                _dnsServer.LogManager.Write(\"DNS Server has started zone refresh for \" + GetZoneTypeName() + \" zone: \" + ToString());\n\n                //get nameservers list with correct zone tranfer protocol\n                List<NameServerAddress> updatedNameServers = new List<NameServerAddress>(primaryNameServers.Count);\n                {\n                    switch (zoneTransferProtocol)\n                    {\n                        case DnsTransportProtocol.Tls:\n                        case DnsTransportProtocol.Quic:\n                            //change name server protocol to TLS/QUIC\n                            foreach (NameServerAddress primaryNameServer in primaryNameServers)\n                            {\n                                if (primaryNameServer.Protocol == zoneTransferProtocol)\n                                    updatedNameServers.Add(primaryNameServer);\n                                else\n                                    updatedNameServers.Add(primaryNameServer.Clone(zoneTransferProtocol));\n                            }\n\n                            break;\n\n                        default:\n                            //change name server protocol to TCP\n                            foreach (NameServerAddress primaryNameServer in primaryNameServers)\n                            {\n                                if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp)\n                                    updatedNameServers.Add(primaryNameServer);\n                                else\n                                    updatedNameServers.Add(primaryNameServer.Clone(DnsTransportProtocol.Tcp));\n                            }\n\n                            break;\n                    }\n                }\n\n                //init XFR DNS Client\n                DnsClient xfrClient = new DnsClient(updatedNameServers);\n                xfrClient.Proxy = _dnsServer.Proxy;\n                xfrClient.PreferIPv6 = _dnsServer.PreferIPv6;\n                xfrClient.Retries = REFRESH_RETRIES;\n                xfrClient.Concurrency = 1;\n\n                DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];\n                DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;\n\n                if (!_resync && (this is not SecondaryForwarderZone)) //skip SOA probe for Secondary Forwarder/Catalog since Forwarder/Catalog is not authoritative for SOA\n                {\n                    //check for update\n                    xfrClient.Timeout = REFRESH_SOA_TIMEOUT;\n\n                    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);\n                    DnsDatagram soaResponse;\n\n                    if (key is null)\n                        soaResponse = await xfrClient.RawResolveAsync(soaRequest);\n                    else\n                        soaResponse = await xfrClient.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE);\n\n                    if (soaResponse.RCODE != DnsResponseCode.NoError)\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server received RCODE=\" + soaResponse.RCODE.ToString() + \" for '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone refresh from: \" + soaResponse.Metadata.NameServer.ToString());\n                        return false;\n                    }\n\n                    if ((soaResponse.Answer.Count < 1) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA) || !_name.Equals(soaResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase))\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server received an empty response for SOA query for '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone refresh from: \" + soaResponse.Metadata.NameServer.ToString());\n                        return false;\n                    }\n\n                    DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];\n                    DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;\n\n                    //compare using sequence space arithmetic\n                    if (!currentSoa.IsZoneUpdateAvailable(receivedSoa))\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server successfully checked for '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone update from: \" + soaResponse.Metadata.NameServer.ToString());\n                        return true;\n                    }\n                }\n\n                //update available; do zone transfer\n                xfrClient.Timeout = REFRESH_XFR_TIMEOUT;\n\n                bool doIXFR = !_isExpired && !_resync;\n\n                while (true)\n                {\n                    DnsQuestionRecord xfrQuestion;\n                    IReadOnlyList<DnsResourceRecord> xfrAuthority;\n\n                    if (doIXFR)\n                    {\n                        xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.IXFR, DnsClass.IN);\n                        xfrAuthority = [currentSoaRecord];\n                    }\n                    else\n                    {\n                        xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.AXFR, DnsClass.IN);\n                        xfrAuthority = null;\n                    }\n\n                    DnsDatagram xfrRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [xfrQuestion], null, xfrAuthority);\n                    DnsDatagram xfrResponse;\n\n                    if (key is null)\n                        xfrResponse = await xfrClient.RawResolveAsync(xfrRequest);\n                    else\n                        xfrResponse = await xfrClient.TsigResolveAsync(xfrRequest, key, REFRESH_TSIG_FUDGE);\n\n                    if (doIXFR && ((xfrResponse.RCODE == DnsResponseCode.NotImplemented) || (xfrResponse.RCODE == DnsResponseCode.Refused)))\n                    {\n                        doIXFR = false;\n                        continue;\n                    }\n\n                    if (xfrResponse.RCODE != DnsResponseCode.NoError)\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server received a zone transfer response (RCODE=\" + xfrResponse.RCODE.ToString() + \") for '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone from: \" + xfrResponse.Metadata.NameServer.ToString());\n                        return false;\n                    }\n\n                    if (xfrResponse.Answer.Count < 1)\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server received an empty response for zone transfer query for '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone from: \" + xfrResponse.Metadata.NameServer.ToString());\n                        return false;\n                    }\n\n                    if (!_name.Equals(xfrResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase) || (xfrResponse.Answer[0].RDATA is not DnsSOARecordData xfrSoa))\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server received invalid response for zone transfer query for '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone from: \" + xfrResponse.Metadata.NameServer.ToString());\n                        return false;\n                    }\n\n                    if (_resync || currentSoa.IsZoneUpdateAvailable(xfrSoa))\n                    {\n                        xfrResponse = xfrResponse.Join(); //join multi message response\n\n                        if (doIXFR)\n                        {\n                            IReadOnlyList<DnsResourceRecord> historyRecords = _dnsServer.AuthZoneManager.SyncIncrementalZoneTransferRecords(_name, xfrResponse.Answer);\n                            if (historyRecords.Count > 0)\n                                await FinalizeIncrementalZoneTransferAsync(historyRecords);\n                            else\n                                await FinalizeZoneTransferAsync(); //AXFR response was received\n                        }\n                        else\n                        {\n                            _dnsServer.AuthZoneManager.SyncZoneTransferRecords(_name, xfrResponse.Answer);\n                            await FinalizeZoneTransferAsync();\n                        }\n\n                        _lastModified = DateTime.UtcNow;\n\n                        if (validateZone)\n                            await ValidateZoneAsync();\n                        else\n                            _validationFailed = false;\n\n                        if (_validationFailed)\n                        {\n                            _dnsServer.LogManager.Write(\"DNS Server refreshed '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone with validation failure from: \" + xfrResponse.Metadata.NameServer.ToString());\n                        }\n                        else\n                        {\n                            //trigger notify\n                            TriggerNotify();\n\n                            _dnsServer.LogManager.Write(\"DNS Server successfully refreshed '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone from: \" + xfrResponse.Metadata.NameServer.ToString());\n                        }\n                    }\n                    else\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server successfully checked for '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone update from: \" + xfrResponse.Metadata.NameServer.ToString());\n                    }\n\n                    return true;\n                }\n            }\n            catch (Exception ex)\n            {\n                _dnsServer.LogManager.Write(\"DNS Server failed to refresh '\" + ToString() + \"' \" + GetZoneTypeName() + \" zone from: \" + primaryNameServers.Join() + \"\\r\\n\" + ex.ToString());\n\n                return false;\n            }\n        }\n\n        private async Task ValidateZoneAsync(CancellationToken cancellationToken = default)\n        {\n            try\n            {\n                DirectDnsClient dnsClient = new DirectDnsClient(_dnsServer);\n                dnsClient.DnssecValidation = true;\n                dnsClient.Timeout = 10000;\n\n                DnsDatagram zoneMdResponse = await dnsClient.ResolveAsync(_name, DnsResourceRecordType.ZONEMD, cancellationToken);\n                IReadOnlyList<DnsZONEMDRecordData> zoneMdList = DnsClient.ParseResponseZONEMD(zoneMdResponse);\n                if (zoneMdList.Count == 0)\n                {\n                    //ZONEMD RRSet does not exists; digest verification cannot occur\n                    _validationFailed = false;\n                    _dnsServer.LogManager.Write(\"ZONEMD validation cannot occur for the \" + GetZoneTypeName() + \" zone '\" + ToString() + \"': ZONEMD RRset does not exists in the zone.\");\n                    return;\n                }\n\n                for (int i = 0; i < zoneMdList.Count; i++)\n                {\n                    for (int j = 0; j < zoneMdList.Count; j++)\n                    {\n                        if (i == j)\n                            continue; //skip comparing self\n\n                        DnsZONEMDRecordData zoneMd = zoneMdList[i];\n                        DnsZONEMDRecordData checkZoneMd = zoneMdList[j];\n\n                        if ((checkZoneMd.Scheme == zoneMd.Scheme) && (checkZoneMd.HashAlgorithm == zoneMd.HashAlgorithm))\n                        {\n                            _validationFailed = true;\n                            _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.\");\n                            return;\n                        }\n                    }\n                }\n\n                DnsDatagram soaResponse = await dnsClient.ResolveAsync(_name, DnsResourceRecordType.SOA, cancellationToken);\n                DnsSOARecordData soa = DnsClient.ParseResponseSOA(soaResponse);\n                if (soa is null)\n                {\n                    _validationFailed = true;\n                    _dnsServer.LogManager.Write(\"ZONEMD validation failed for the \" + GetZoneTypeName() + \" zone '\" + ToString() + \"': failed to find SOA record.\");\n                    return;\n                }\n\n                using MemoryStream hashStream = new MemoryStream(4096);\n                byte[] computedDigestSHA384 = null;\n                byte[] computedDigestSHA512 = null;\n                bool zoneSerialized = false;\n\n                foreach (DnsZONEMDRecordData zoneMd in zoneMdList)\n                {\n                    if (soa.Serial != zoneMd.Serial)\n                        continue;\n\n                    if (zoneMd.Scheme != ZoneMdScheme.Simple)\n                        continue;\n\n                    byte[] computedDigest;\n\n                    switch (zoneMd.HashAlgorithm)\n                    {\n                        case ZoneMdHashAlgorithm.SHA384:\n                            if (zoneMd.Digest.Length != 48)\n                                continue;\n\n                            if (computedDigestSHA384 is null)\n                            {\n                                if (!zoneSerialized)\n                                {\n                                    SerializeZoneTo(hashStream);\n                                    zoneSerialized = true;\n                                }\n\n                                hashStream.Position = 0;\n                                computedDigestSHA384 = SHA384.HashData(hashStream);\n                            }\n\n                            computedDigest = computedDigestSHA384;\n                            break;\n\n                        case ZoneMdHashAlgorithm.SHA512:\n                            if (zoneMd.Digest.Length != 64)\n                                continue;\n\n                            if (computedDigestSHA512 is null)\n                            {\n                                if (!zoneSerialized)\n                                {\n                                    SerializeZoneTo(hashStream);\n                                    zoneSerialized = true;\n                                }\n\n                                hashStream.Position = 0;\n                                computedDigestSHA512 = SHA512.HashData(hashStream);\n                            }\n\n                            computedDigest = computedDigestSHA512;\n                            break;\n\n                        default:\n                            continue;\n                    }\n\n                    if (computedDigest.ListEquals(zoneMd.Digest))\n                    {\n                        //validation successfull\n                        _validationFailed = false;\n                        _dnsServer.LogManager.Write(\"ZONEMD validation was completed successfully for the \" + GetZoneTypeName() + \" zone: \" + ToString());\n                        return;\n                    }\n                }\n\n                //validation failed\n                _validationFailed = true;\n                _dnsServer.LogManager.Write(\"ZONEMD validation failed for the \" + GetZoneTypeName() + \" zone '\" + ToString() + \"': none of the ZONEMD records could successfully validate the zone.\");\n            }\n            catch (Exception ex)\n            {\n                //validation failed\n                _validationFailed = true;\n                _dnsServer.LogManager.Write(\"ZONEMD validation failed for the \" + GetZoneTypeName() + \" zone '\" + ToString() + \"':\\r\\n\" + ex.ToString());\n            }\n        }\n\n        private void SerializeZoneTo(MemoryStream hashStream)\n        {\n            //list zone records for ZONEMD Simple scheme\n            List<DnsResourceRecord> records;\n            {\n                List<DnsResourceRecord> allZoneRecords = new List<DnsResourceRecord>();\n\n                _dnsServer.AuthZoneManager.ListAllZoneRecords(_name, allZoneRecords);\n\n                records = new List<DnsResourceRecord>(allZoneRecords.Count);\n\n                foreach (DnsResourceRecord record in allZoneRecords)\n                {\n                    switch (record.Type)\n                    {\n                        case DnsResourceRecordType.NS:\n                            records.Add(record);\n\n                            IReadOnlyList<DnsResourceRecord> glueRecords = record.GetAuthNSRecordInfo().GlueRecords;\n                            if (glueRecords is not null)\n                                records.AddRange(glueRecords);\n\n                            break;\n\n                        case DnsResourceRecordType.RRSIG:\n                            if (record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase) && (record.RDATA is DnsRRSIGRecordData rdata) && (rdata.TypeCovered == DnsResourceRecordType.ZONEMD))\n                                break; //skip RRSIG covering the apex ZONEMD\n\n                            records.Add(record);\n                            break;\n\n                        case DnsResourceRecordType.ZONEMD:\n                            if (record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase))\n                                break; //skip apex ZONEMD\n\n                            records.Add(record);\n                            break;\n\n                        default:\n                            records.Add(record);\n                            break;\n                    }\n                }\n            }\n\n            //group records into zones by DNS name\n            List<KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>>> zones = new List<KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>>>(DnsResourceRecord.GroupRecords(records, true));\n\n            //sort zones by canonical DNS name\n            zones.Sort(delegate (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> x, KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> y)\n            {\n                return DnsNSECRecordData.CanonicalComparison(x.Key, y.Key);\n            });\n\n            //start serialization, zone by zone\n            using MemoryStream rrBuffer = new MemoryStream(512);\n\n            foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> zone in zones)\n            {\n                //list all RRSets for current zone owner name\n                List<KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>>> rrSets = new List<KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>>>(zone.Value);\n\n                //RRsets having the same owner name MUST be numerically ordered, in ascending order, by their numeric RR TYPE\n                rrSets.Sort(delegate (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> x, KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> y)\n                {\n                    return x.Key.CompareTo(y.Key);\n                });\n\n                //serialize records\n                List<CanonicallySerializedResourceRecord> rrList = new List<CanonicallySerializedResourceRecord>(rrSets.Count * 4);\n\n                foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> rrSet in rrSets)\n                {\n                    //serialize current RRSet records\n                    List<CanonicallySerializedResourceRecord> serializedResourceRecords = new List<CanonicallySerializedResourceRecord>(rrSet.Value.Count);\n\n                    foreach (DnsResourceRecord record in rrSet.Value)\n                        serializedResourceRecords.Add(CanonicallySerializedResourceRecord.Create(record.Name, record.Type, record.Class, record.OriginalTtlValue, record.RDATA, rrBuffer));\n\n                    //Canonical RR Ordering by sorting RDATA portion of the canonical form of each RR\n                    serializedResourceRecords.Sort();\n\n                    foreach (CanonicallySerializedResourceRecord serializedResourceRecord in serializedResourceRecords)\n                        serializedResourceRecord.WriteTo(hashStream);\n                }\n            }\n        }\n\n        protected virtual Task FinalizeZoneTransferAsync()\n        {\n            ClearZoneHistory();\n\n            return Task.CompletedTask;\n        }\n\n        protected virtual Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList<DnsResourceRecord> historyRecords)\n        {\n            CommitZoneHistory(historyRecords);\n\n            return Task.CompletedTask;\n        }\n\n        #endregion\n\n        #region public\n\n        public override string GetZoneTypeName()\n        {\n            return \"Secondary\";\n        }\n\n        public void TriggerRefresh(int refreshInterval = REFRESH_TIMER_INTERVAL)\n        {\n            if (Disabled)\n                return;\n\n            if (_refreshTimerTriggered)\n                return;\n\n            _refreshTimerTriggered = true;\n            ResetRefreshTimer(refreshInterval);\n        }\n\n        public void TriggerResync()\n        {\n            if (_refreshTimerTriggered)\n                return;\n\n            _resync = true;\n\n            _refreshTimerTriggered = true;\n            ResetRefreshTimer(0);\n        }\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            throw new InvalidOperationException(\"Cannot set records in \" + GetZoneTypeName() + \" zone.\");\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            throw new InvalidOperationException(\"Cannot add record in \" + GetZoneTypeName() + \" zone.\");\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)\n        {\n            throw new InvalidOperationException(\"Cannot delete record in \" + GetZoneTypeName() + \" zone.\");\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            throw new InvalidOperationException(\"Cannot delete records in \" + GetZoneTypeName() + \" zone.\");\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            throw new InvalidOperationException(\"Cannot update record in \" + GetZoneTypeName() + \" zone.\");\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool Disabled\n        {\n            get { return base.Disabled; }\n            set\n            {\n                if (base.Disabled == value)\n                    return;\n\n                base.Disabled = value; //set value early to be able to use it for notify\n\n                if (value)\n                {\n                    DisableNotifyTimer();\n                    ResetRefreshTimer(Timeout.Infinite);\n                }\n                else\n                {\n                    TriggerNotify();\n                    TriggerRefresh();\n                }\n            }\n        }\n\n        public override bool OverrideCatalogNotify\n        {\n            get\n            {\n                //return true so that notification does not trigger when secondary zone is a member of catalog\n                return true;\n            }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public virtual bool OverrideCatalogPrimaryNameServers\n        {\n            get { return _overrideCatalogPrimaryNameServers; }\n            set { _overrideCatalogPrimaryNameServers = value; }\n        }\n\n        public override AuthZoneNotify Notify\n        {\n            get { return base.Notify; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones:\n                        throw new ArgumentException(\"The Notify option is invalid for \" + GetZoneTypeName() + \" zones: \" + value.ToString(), nameof(Notify));\n                }\n\n                base.Notify = value;\n            }\n        }\n\n        public override AuthZoneUpdate Update\n        {\n            get { return base.Update; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneUpdate.AllowOnlyZoneNameServers:\n                    case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        throw new ArgumentException(\"The Dynamic Updates option is invalid for Secondary zones: \" + value.ToString(), nameof(Update));\n                }\n\n                base.Update = value;\n            }\n        }\n\n        public virtual IReadOnlyList<NameServerAddress> PrimaryNameServerAddresses\n        {\n            get { return _primaryNameServerAddresses; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                    _primaryNameServerAddresses = null;\n                else if (value.Count > byte.MaxValue)\n                    throw new ArgumentOutOfRangeException(nameof(PrimaryNameServerAddresses), \"Name server addresses cannot have more than 255 entries.\");\n                else\n                    _primaryNameServerAddresses = value;\n\n                //update catalog zone property\n                if (!Disabled)\n                    CatalogZone?.SetPrimaryAddressesProperty(_primaryNameServerAddresses, _name);\n            }\n        }\n\n        public DnsTransportProtocol PrimaryZoneTransferProtocol\n        {\n            get { return _primaryZoneTransferProtocol; }\n            set\n            {\n                switch (value)\n                {\n                    case DnsTransportProtocol.Tcp:\n                    case DnsTransportProtocol.Tls:\n                    case DnsTransportProtocol.Quic:\n                        _primaryZoneTransferProtocol = value;\n\n                        //update catalog zone property\n                        if (!Disabled)\n                        {\n                            CatalogZone catalogZone = CatalogZone;\n                            if (catalogZone is not null)\n                            {\n                                if (_primaryZoneTransferProtocol != DnsTransportProtocol.Tcp)\n                                    catalogZone.SetPrimaryZoneTransferProtocolProperty(_primaryZoneTransferProtocol, _name); //update member zone custom property\n                                else\n                                    catalogZone.SetPrimaryZoneTransferProtocolProperty(null, _name); //remove member zone custom property\n                            }\n                        }\n                        break;\n\n                    default:\n                        throw new NotSupportedException(\"Zone transfer protocol is not supported: XFR-over-\" + value.ToString().ToUpper());\n                }\n            }\n        }\n\n        public string PrimaryZoneTransferTsigKeyName\n        {\n            get { return _primaryZoneTransferTsigKeyName; }\n            set\n            {\n                if (value is null)\n                    _primaryZoneTransferTsigKeyName = string.Empty;\n                else\n                    _primaryZoneTransferTsigKeyName = value;\n\n                //update catalog zone property\n                if (!Disabled)\n                {\n                    CatalogZone catalogZone = CatalogZone;\n                    if (catalogZone is not null)\n                    {\n                        if (_primaryZoneTransferTsigKeyName.Length > 0)\n                            catalogZone.SetPrimaryZoneTransferTsigKeyNameProperty(_primaryZoneTransferTsigKeyName, _name); //update member zone custom property\n                        else\n                            catalogZone.SetPrimaryZoneTransferTsigKeyNameProperty(null, _name); //remove member zone custom property\n                    }\n                }\n            }\n        }\n\n        public DateTime Expiry\n        { get { return _expiry; } }\n\n        public bool IsExpired\n        { get { return _isExpired; } }\n\n        public virtual bool ValidateZone\n        {\n            get { return _validateZone; }\n            set\n            {\n                _validateZone = value;\n\n                //update catalog zone property\n                if (!Disabled)\n                {\n                    CatalogZone catalogZone = CatalogZone;\n                    if (catalogZone is not null)\n                    {\n                        if (_validateZone)\n                            catalogZone.SetZoneMdValidationProperty(_validateZone, _name); //update member zone custom property\n                        else\n                            catalogZone.SetZoneMdValidationProperty(null, _name); //remove member zone custom property\n                    }\n                }\n            }\n        }\n\n        public bool ValidationFailed\n        { get { return _validationFailed; } }\n\n        public override bool IsActive\n        {\n            get { return !Disabled && !_isExpired && !_validationFailed; }\n        }\n\n        public IReadOnlyCollection<DnssecPrivateKey> DnssecPrivateKeys\n        {\n            get { return _dnssecPrivateKeys; }\n            set { _dnssecPrivateKeys = value; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/StubZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    class StubZone : ApexZone\n    {\n        #region variables\n\n        readonly object _refreshTimerLock = new object();\n        Timer _refreshTimer;\n        bool _refreshTimerTriggered;\n        const int REFRESH_TIMER_INTERVAL = 5000;\n\n        const int REFRESH_TIMEOUT = 10000;\n        const int REFRESH_RETRIES = 5;\n\n        IReadOnlyList<NameServerAddress> _primaryNameServerAddresses;\n\n        DateTime _expiry;\n        bool _isExpired;\n\n        bool _resync;\n\n        #endregion\n\n        #region constructor\n\n        public StubZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)\n            : base(dnsServer, zoneInfo)\n        {\n            _primaryNameServerAddresses = zoneInfo.PrimaryNameServerAddresses;\n\n            _expiry = zoneInfo.Expiry;\n            _isExpired = DateTime.UtcNow > _expiry;\n\n            _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n        }\n\n        private StubZone(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses)\n            : base(dnsServer, name)\n        {\n            PrimaryNameServerAddresses = primaryNameServerAddresses?.Convert(delegate (NameServerAddress nameServer)\n            {\n                if (nameServer.Protocol != DnsTransportProtocol.Udp)\n                    nameServer = nameServer.Clone(DnsTransportProtocol.Udp);\n\n                return nameServer;\n            });\n\n            _isExpired = true; //new stub zone is considered expired till it refreshes\n\n            _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite);\n        }\n\n        #endregion\n\n        #region static\n\n        public static async Task<StubZone> CreateAsync(DnsServer dnsServer, string name, IReadOnlyList<NameServerAddress> primaryNameServerAddresses = null, bool ignoreSoaFailure = false)\n        {\n            StubZone stubZone = new StubZone(dnsServer, name, primaryNameServerAddresses);\n\n            try\n            {\n                DnsDatagram soaResponse;\n\n                DnsQuestionRecord soaQuestion = new DnsQuestionRecord(name, DnsResourceRecordType.SOA, DnsClass.IN);\n\n                if (stubZone.PrimaryNameServerAddresses is null)\n                {\n                    soaResponse = await stubZone._dnsServer.DirectQueryAsync(soaQuestion);\n                }\n                else\n                {\n                    DnsClient dnsClient = new DnsClient(stubZone.PrimaryNameServerAddresses);\n                    List<Task> tasks = new List<Task>(dnsClient.Servers.Count);\n\n                    foreach (NameServerAddress nameServerAddress in dnsClient.Servers)\n                    {\n                        if (nameServerAddress.IsIPEndPointStale)\n                            tasks.Add(nameServerAddress.ResolveIPAddressAsync(stubZone._dnsServer, stubZone._dnsServer.PreferIPv6));\n                    }\n\n                    await Task.WhenAll(tasks);\n\n                    dnsClient.Proxy = stubZone._dnsServer.Proxy;\n                    dnsClient.PreferIPv6 = stubZone._dnsServer.PreferIPv6;\n\n                    DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [soaQuestion], null, null, null, dnsServer.UdpPayloadSize);\n\n                    soaResponse = await dnsClient.RawResolveAsync(soaRequest);\n                }\n\n                if ((soaResponse.Answer.Count == 0) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA))\n                    throw new DnsServerException(\"DNS Server did not receive SOA record in response from any of the primary name servers for: \" + name);\n\n                DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];\n                DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;\n\n                DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum);\n                DnsResourceRecord soaRecord = new DnsResourceRecord(stubZone._name, DnsResourceRecordType.SOA, DnsClass.IN, receivedSoaRecord.TTL, soa);\n\n                stubZone._entries[DnsResourceRecordType.SOA] = [soaRecord];\n            }\n            catch\n            {\n                if (!ignoreSoaFailure)\n                    throw;\n\n                //continue with dummy SOA\n                DnsSOARecordData soa = new DnsSOARecordData(stubZone._dnsServer.ServerDomain, \"invalid\", 0, 300, 60, 604800, 900);\n                DnsResourceRecord soaRecord = new DnsResourceRecord(stubZone._name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);\n                soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n\n                stubZone._entries[DnsResourceRecordType.SOA] = [soaRecord];\n            }\n\n            return stubZone;\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        protected override void Dispose(bool disposing)\n        {\n            try\n            {\n                if (_disposed)\n                    return;\n\n                if (disposing)\n                {\n                    lock (_refreshTimerLock)\n                    {\n                        if (_refreshTimer != null)\n                        {\n                            _refreshTimer.Dispose();\n                            _refreshTimer = null;\n                        }\n                    }\n                }\n\n                _disposed = true;\n            }\n            finally\n            {\n                base.Dispose(disposing);\n            }\n        }\n\n        #endregion\n\n        #region private\n\n        private void RefreshTimerCallback(object state)\n        {\n            //refresh zone in DNS server's resolver thread pool\n            if (!_dnsServer.TryQueueResolverTask(async delegate (object state)\n            {\n                try\n                {\n                    if (Disabled && !_resync)\n                        return;\n\n                    _isExpired = DateTime.UtcNow > _expiry;\n\n                    //get primary name server addresses\n                    IReadOnlyList<NameServerAddress> primaryNameServers = await GetResolvedPrimaryNameServerAddressesAsync();\n\n                    if (primaryNameServers.Count == 0)\n                    {\n                        _dnsServer.LogManager.Write(\"DNS Server could not find primary name server IP addresses for Stub zone: \" + ToString());\n\n                        //set timer for retry\n                        ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000);\n                        _syncFailed = true;\n                        return;\n                    }\n\n                    //refresh zone\n                    if (await RefreshZoneAsync(primaryNameServers))\n                    {\n                        //zone refreshed; set timer for refresh\n                        DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;\n                        ResetRefreshTimer(Math.Max(latestSoa.Refresh, _dnsServer.AuthZoneManager.MinSoaRefresh) * 1000);\n                        _syncFailed = false;\n                        _expiry = DateTime.UtcNow.AddSeconds(latestSoa.Expire);\n                        _isExpired = false;\n                        _resync = false;\n                        _dnsServer.AuthZoneManager.SaveZoneFile(_name);\n                        return;\n                    }\n\n                    //no response from any of the name servers; set timer for retry\n                    ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000);\n                    _syncFailed = true;\n                }\n                catch (Exception ex)\n                {\n                    _dnsServer.LogManager.Write(ex);\n\n                    //set timer for retry\n                    ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000);\n                    _syncFailed = true;\n                }\n                finally\n                {\n                    _refreshTimerTriggered = false;\n                }\n            })\n            )\n            {\n                //failed to queue refresh zone task; try again in some time\n                lock (_refreshTimerLock)\n                {\n                    _refreshTimer?.Change(REFRESH_TIMER_INTERVAL, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void ResetRefreshTimer(long dueTime)\n        {\n            lock (_refreshTimerLock)\n            {\n                _refreshTimer?.Change(dueTime, Timeout.Infinite);\n            }\n        }\n\n        private async Task<bool> RefreshZoneAsync(IReadOnlyList<NameServerAddress> nameServers)\n        {\n            try\n            {\n                _dnsServer.LogManager.Write(\"DNS Server has started zone refresh for Stub zone: \" + ToString());\n\n                DnsClient client = new DnsClient(nameServers);\n\n                client.Proxy = _dnsServer.Proxy;\n                client.PreferIPv6 = _dnsServer.PreferIPv6;\n                client.Timeout = REFRESH_TIMEOUT;\n                client.Retries = REFRESH_RETRIES;\n                client.Concurrency = 1;\n\n                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);\n                DnsDatagram soaResponse = await client.RawResolveAsync(soaRequest);\n\n                if (soaResponse.RCODE != DnsResponseCode.NoError)\n                {\n                    _dnsServer.LogManager.Write(\"DNS Server received RCODE=\" + soaResponse.RCODE.ToString() + \" for '\" + ToString() + \"' Stub zone refresh from: \" + soaResponse.Metadata.NameServer.ToString());\n\n                    return false;\n                }\n\n                if ((soaResponse.Answer.Count < 1) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA) || !_name.Equals(soaResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase))\n                {\n                    _dnsServer.LogManager.Write(\"DNS Server received an empty response for SOA query for '\" + ToString() + \"' Stub zone refresh from: \" + soaResponse.Metadata.NameServer.ToString());\n\n                    return false;\n                }\n\n                DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0];\n                DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0];\n\n                DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData;\n                DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData;\n\n                //compare using sequence space arithmetic\n                if (!_resync && !currentSoa.IsZoneUpdateAvailable(receivedSoa))\n                {\n                    _dnsServer.LogManager.Write(\"DNS Server successfully checked for '\" + ToString() + \"' Stub zone update from: \" + soaResponse.Metadata.NameServer.ToString());\n\n                    return true;\n                }\n\n                //update available; do zone sync with TCP transport\n                List<NameServerAddress> tcpNameServers = new List<NameServerAddress>();\n\n                foreach (NameServerAddress nameServer in nameServers)\n                    tcpNameServers.Add(nameServer.Clone(DnsTransportProtocol.Tcp));\n\n                client = new DnsClient(tcpNameServers);\n\n                client.Proxy = _dnsServer.Proxy;\n                client.PreferIPv6 = _dnsServer.PreferIPv6;\n                client.Timeout = REFRESH_TIMEOUT;\n                client.Retries = REFRESH_RETRIES;\n                client.Concurrency = 1;\n\n                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) });\n                DnsDatagram nsResponse = await client.RawResolveAsync(nsRequest);\n\n                if (nsResponse.RCODE != DnsResponseCode.NoError)\n                {\n                    _dnsServer.LogManager.Write(\"DNS Server received RCODE=\" + nsResponse.RCODE.ToString() + \" for '\" + ToString() + \"' Stub zone refresh from: \" + nsResponse.Metadata.NameServer.ToString());\n\n                    return false;\n                }\n\n                if (nsResponse.Answer.Count < 1)\n                {\n                    _dnsServer.LogManager.Write(\"DNS Server received an empty response for NS query for '\" + ToString() + \"' Stub zone from: \" + nsResponse.Metadata.NameServer.ToString());\n\n                    return false;\n                }\n\n                //prepare sync records\n                List<DnsResourceRecord> nsRecords = new List<DnsResourceRecord>(nsResponse.Answer.Count);\n\n                foreach (DnsResourceRecord record in nsResponse.Answer)\n                {\n                    if ((record.Type == DnsResourceRecordType.NS) && record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase))\n                    {\n                        record.SyncGlueRecords(nsResponse.Additional);\n                        nsRecords.Add(record);\n                    }\n                }\n\n                receivedSoaRecord.CopyRecordInfoFrom(currentSoaRecord);\n\n                //sync records\n                _entries[DnsResourceRecordType.NS] = nsRecords;\n                _entries[DnsResourceRecordType.SOA] = [receivedSoaRecord];\n\n                _lastModified = DateTime.UtcNow;\n\n                _dnsServer.LogManager.Write(\"DNS Server successfully refreshed '\" + ToString() + \"' Stub zone from: \" + nsResponse.Metadata.NameServer.ToString());\n\n                return true;\n            }\n            catch (Exception ex)\n            {\n                string strNameServers = null;\n\n                foreach (NameServerAddress nameServer in nameServers)\n                {\n                    if (strNameServers == null)\n                        strNameServers = nameServer.ToString();\n                    else\n                        strNameServers += \", \" + nameServer.ToString();\n                }\n\n                _dnsServer.LogManager.Write(\"DNS Server failed to refresh '\" + ToString() + \"' Stub zone from: \" + strNameServers + \"\\r\\n\" + ex.ToString());\n\n                return false;\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public override string GetZoneTypeName()\n        {\n            return \"Stub\";\n        }\n\n        public void TriggerRefresh(int refreshInterval = REFRESH_TIMER_INTERVAL)\n        {\n            if (Disabled)\n                return;\n\n            if (_refreshTimerTriggered)\n                return;\n\n            _refreshTimerTriggered = true;\n            ResetRefreshTimer(refreshInterval);\n        }\n\n        public void TriggerResync()\n        {\n            if (_refreshTimerTriggered)\n                return;\n\n            _resync = true;\n\n            _refreshTimerTriggered = true;\n            ResetRefreshTimer(0);\n        }\n\n        public override void SetRecords(DnsResourceRecordType type, IReadOnlyList<DnsResourceRecord> records)\n        {\n            throw new InvalidOperationException(\"Cannot set records in Stub zone.\");\n        }\n\n        public override bool AddRecord(DnsResourceRecord record)\n        {\n            throw new InvalidOperationException(\"Cannot add record in Stub zone.\");\n        }\n\n        public override bool DeleteRecords(DnsResourceRecordType type)\n        {\n            throw new InvalidOperationException(\"Cannot delete record in Stub zone.\");\n        }\n\n        public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)\n        {\n            throw new InvalidOperationException(\"Cannot delete records in Stub zone.\");\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            throw new InvalidOperationException(\"Cannot update record in Stub zone.\");\n        }\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)\n        {\n            return []; //stub zone has no authority so cant return any records as query response to allow generating referral response\n        }\n\n        #endregion\n\n        #region properties\n\n        public override bool Disabled\n        {\n            get { return base.Disabled; }\n            set\n            {\n                if (base.Disabled == value)\n                    return;\n\n                base.Disabled = value; //set value early to be able to use it for refresh\n\n                if (value)\n                    ResetRefreshTimer(Timeout.Infinite);\n                else\n                    TriggerRefresh();\n            }\n        }\n\n        public override bool OverrideCatalogZoneTransfer\n        {\n            get { throw new InvalidOperationException(); }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override bool OverrideCatalogNotify\n        {\n            get { throw new InvalidOperationException(); }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneQueryAccess QueryAccess\n        {\n            get { return base.QueryAccess; }\n            set\n            {\n                switch (value)\n                {\n                    case AuthZoneQueryAccess.AllowOnlyZoneNameServers:\n                    case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL:\n                        throw new ArgumentException(\"The Query Access option is invalid for Stub zones: \" + value.ToString(), nameof(QueryAccess));\n                }\n\n                base.QueryAccess = value;\n            }\n        }\n\n        public override AuthZoneTransfer ZoneTransfer\n        {\n            get { return base.ZoneTransfer; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneNotify Notify\n        {\n            get { return base.Notify; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public override AuthZoneUpdate Update\n        {\n            get { return base.Update; }\n            set { throw new InvalidOperationException(); }\n        }\n\n        public IReadOnlyList<NameServerAddress> PrimaryNameServerAddresses\n        {\n            get { return _primaryNameServerAddresses; }\n            set\n            {\n                if ((value is null) || (value.Count == 0))\n                {\n                    _primaryNameServerAddresses = null;\n                }\n                else if (value.Count > byte.MaxValue)\n                {\n                    throw new ArgumentOutOfRangeException(nameof(PrimaryNameServerAddresses), \"Name server addresses cannot have more than 255 entries.\");\n                }\n                else\n                {\n                    foreach (NameServerAddress nameServer in value)\n                    {\n                        if (nameServer.Port != 53)\n                            throw new ArgumentException(\"Name server address must use port 53 for Stub zones.\", nameof(PrimaryNameServerAddresses));\n                    }\n\n                    _primaryNameServerAddresses = value;\n                }\n\n                //update catalog zone property\n                if (!Disabled)\n                    CatalogZone?.SetPrimaryAddressesProperty(_primaryNameServerAddresses, _name);\n            }\n        }\n\n        public DateTime Expiry\n        { get { return _expiry; } }\n\n        public bool IsExpired\n        { get { return _isExpired; } }\n\n        public override bool IsActive\n        {\n            get { return !Disabled && !_isExpired; }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/SubDomainZone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns.ResourceRecords;\nusing System.Collections.Generic;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    abstract class SubDomainZone : AuthZone\n    {\n        #region variables\n\n        readonly ApexZone _authoritativeZone;\n\n        #endregion\n\n        #region constructor\n\n        protected SubDomainZone(ApexZone authoritativeZone, string name)\n            : base(name)\n        {\n            _authoritativeZone = authoritativeZone;\n        }\n\n        #endregion\n\n        #region public\n\n        public void AutoUpdateState()\n        {\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n            {\n                foreach (DnsResourceRecord record in entry.Value)\n                {\n                    if (!record.GetAuthGenericRecordInfo().Disabled)\n                    {\n                        Disabled = false;\n                        return;\n                    }\n                }\n            }\n\n            Disabled = true;\n        }\n\n        #endregion\n\n        #region properties\n\n        public ApexZone AuthoritativeZone\n        { get { return _authoritativeZone; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Dns/Zones/Zone.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.Dns.Zones\n{\n    abstract class Zone\n    {\n        #region variables\n\n        protected readonly string _name;\n        protected readonly ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> _entries;\n\n        #endregion\n\n        #region constructor\n\n        protected Zone(string name)\n        {\n            _name = name.ToLowerInvariant();\n            _entries = new ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>(1, 5);\n        }\n\n        protected Zone(string name, int capacity)\n        {\n            _name = name.ToLowerInvariant();\n            _entries = new ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>>(1, capacity);\n        }\n\n        protected Zone(string name, ConcurrentDictionary<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entries)\n        {\n            _name = name.ToLowerInvariant();\n            _entries = entries;\n        }\n\n        #endregion\n\n        #region static\n\n        public static string GetReverseZone(IPAddress address, IPAddress subnetMask)\n        {\n            return GetReverseZone(address, subnetMask.GetSubnetMaskWidth());\n        }\n\n        public static string GetReverseZone(IPAddress address, int subnetMaskWidth)\n        {\n            int addressByteCount = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(subnetMaskWidth) / 8));\n            byte[] addressBytes = address.GetAddressBytes();\n            string reverseZone = \"\";\n\n            switch (address.AddressFamily)\n            {\n                case AddressFamily.InterNetwork:\n                    for (int i = 0; i < addressByteCount; i++)\n                        reverseZone = addressBytes[i] + \".\" + reverseZone;\n\n                    reverseZone += \"in-addr.arpa\";\n                    break;\n\n                case AddressFamily.InterNetworkV6:\n                    for (int i = 0; i < addressByteCount; i++)\n                        reverseZone = (addressBytes[i] & 0x0F).ToString(\"x\") + \".\" + (addressBytes[i] >> 4).ToString(\"x\") + \".\" + reverseZone;\n\n                    reverseZone += \"ip6.arpa\";\n                    break;\n\n                default:\n                    throw new NotSupportedException(\"AddressFamily not supported.\");\n            }\n\n            return reverseZone;\n        }\n\n        #endregion\n\n        #region public\n\n        public virtual void ListAllRecords(List<DnsResourceRecord> records)\n        {\n            foreach (KeyValuePair<DnsResourceRecordType, IReadOnlyList<DnsResourceRecord>> entry in _entries)\n                records.AddRange(entry.Value);\n        }\n\n        public abstract bool ContainsNameServerRecords();\n\n        public override string ToString()\n        {\n            return _name;\n        }\n\n        #endregion\n\n        #region properties\n\n        public string Name\n        { get { return _name; } }\n\n        public virtual bool IsEmpty\n        { get { return _entries.IsEmpty; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/DnsServerCore.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<RepositoryType></RepositoryType>\n\t\t<Description></Description>\n\t\t<PackageId>DnsServer</PackageId>\n\t\t<Version>14.3</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\DnsServerCore.ApplicationCommon\\DnsServerCore.ApplicationCommon.csproj\" />\n\t\t<ProjectReference Include=\"..\\DnsServerCore.HttpApi\\DnsServerCore.HttpApi.csproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.ByteTree\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.ByteTree.dll</HintPath>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.IO\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.IO.dll</HintPath>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Security.OTP\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Security.OTP.dll</HintPath>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"BouncyCastle.Cryptography\" Version=\"2.6.2\" />\n\t\t<PackageReference Include=\"QRCoder\" Version=\"1.7.0\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<None Remove=\"dohwww\\css\\bootstrap.min.css\" />\n\t\t<None Remove=\"dohwww\\css\\bootstrap.min.css.map\" />\n\t\t<None Remove=\"dohwww\\css\\main.css\" />\n\t\t<None Remove=\"dohwww\\favicon.ico\" />\n\t\t<None Remove=\"dohwww\\img\\firefox-doh.png\" />\n\t\t<None Remove=\"dohwww\\img\\logo.png\" />\n\t\t<None Remove=\"dohwww\\index.html\" />\n\t\t<None Remove=\"dohwww\\js\\jquery.min.js\" />\n\t\t<None Remove=\"dohwww\\js\\main.js\" />\n\t\t<None Remove=\"dohwww\\robots.txt\" />\n\t\t<None Remove=\"named.root\" />\n\t\t<None Remove=\"root-anchors.xml\" />\n\t\t<None Remove=\"www\\css\\bootstrap-theme.min.css\" />\n\t\t<None Remove=\"www\\css\\bootstrap-theme.min.css.map\" />\n\t\t<None Remove=\"www\\css\\bootstrap.min.css\" />\n\t\t<None Remove=\"www\\css\\bootstrap.min.css.map\" />\n\t\t<None Remove=\"www\\css\\font-awesome.min.css\" />\n\t\t<None Remove=\"www\\css\\main.css\" />\n\t\t<None Remove=\"www\\favicon.ico\" />\n\t\t<None Remove=\"www\\fonts\\fontawesome-webfont.eot\" />\n\t\t<None Remove=\"www\\fonts\\fontawesome-webfont.svg\" />\n\t\t<None Remove=\"www\\fonts\\fontawesome-webfont.ttf\" />\n\t\t<None Remove=\"www\\fonts\\fontawesome-webfont.woff\" />\n\t\t<None Remove=\"www\\fonts\\fontawesome-webfont.woff2\" />\n\t\t<None Remove=\"www\\fonts\\FontAwesome.otf\" />\n\t\t<None Remove=\"www\\fonts\\glyphicons-halflings-regular.eot\" />\n\t\t<None Remove=\"www\\fonts\\glyphicons-halflings-regular.svg\" />\n\t\t<None Remove=\"www\\fonts\\glyphicons-halflings-regular.ttf\" />\n\t\t<None Remove=\"www\\fonts\\glyphicons-halflings-regular.woff\" />\n\t\t<None Remove=\"www\\fonts\\glyphicons-halflings-regular.woff2\" />\n\t\t<None Remove=\"www\\img\\loader-small.gif\" />\n\t\t<None Remove=\"www\\img\\loader.gif\" />\n\t\t<None Remove=\"www\\img\\logo.png\" />\n\t\t<None Remove=\"www\\img\\logo25x25.png\" />\n\t\t<None Remove=\"www\\index.html\" />\n\t\t<None Remove=\"www\\json\\quick-block-lists-builtin.json\" />\n\t\t<None Remove=\"www\\json\\quick-forwarders-list-builtin.json\" />\n\t\t<None Remove=\"www\\json\\readme.txt\" />\n\t\t<None Remove=\"www\\js\\apps.js\" />\n\t\t<None Remove=\"www\\js\\auth.js\" />\n\t\t<None Remove=\"www\\js\\bootstrap.min.js\" />\n\t\t<None Remove=\"www\\js\\Chart.min.js\" />\n\t\t<None Remove=\"www\\js\\cluster.js\" />\n\t\t<None Remove=\"www\\js\\common.js\" />\n\t\t<None Remove=\"www\\js\\dhcp.js\" />\n\t\t<None Remove=\"www\\js\\dnsclient.js\" />\n\t\t<None Remove=\"www\\js\\jquery.min.js\" />\n\t\t<None Remove=\"www\\js\\logs.js\" />\n\t\t<None Remove=\"www\\js\\main.js\" />\n\t\t<None Remove=\"www\\js\\moment.min.js\" />\n\t\t<None Remove=\"www\\js\\other-zones.js\" />\n\t\t<None Remove=\"www\\js\\zone.js\" />\n\t\t<None Remove=\"www\\robots.txt\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Content Include=\"dohwww\\css\\bootstrap.min.css\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\css\\bootstrap.min.css.map\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\css\\main.css\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\favicon.ico\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\img\\firefox-doh.png\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\img\\logo.png\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\index.html\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\js\\jquery.min.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\js\\main.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"dohwww\\robots.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"named.root\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"root-anchors.xml\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\favicon.ico\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\css\\bootstrap.min.css\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\css\\bootstrap.min.css.map\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\css\\font-awesome.min.css\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\css\\main.css\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\fontawesome-webfont.eot\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\fontawesome-webfont.svg\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\fontawesome-webfont.ttf\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\fontawesome-webfont.woff\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\fontawesome-webfont.woff2\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\FontAwesome.otf\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\glyphicons-halflings-regular.eot\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\glyphicons-halflings-regular.svg\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\glyphicons-halflings-regular.ttf\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\glyphicons-halflings-regular.woff\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\fonts\\glyphicons-halflings-regular.woff2\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\img\\loader-small.gif\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\img\\loader.gif\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\img\\logo.png\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\img\\logo25x25.png\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\index.html\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\json\\quick-block-lists-builtin.json\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\json\\quick-forwarders-list-builtin.json\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\json\\readme.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\json\\dnsclient-server-list-builtin.json\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\apps.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\auth.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\bootstrap.min.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\Chart.min.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\cluster.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\common.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\dnsclient.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\jquery.min.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\logs.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\main.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\moment.min.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\other-zones.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\zone.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\js\\dhcp.js\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"www\\robots.txt\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "DnsServerCore/DnsWebService.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Cluster;\nusing DnsServerCore.Dhcp;\nusing DnsServerCore.Dns;\nusing DnsServerCore.Dns.Applications;\nusing DnsServerCore.Dns.Dnssec;\nusing DnsServerCore.Dns.Zones;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Connections;\nusing Microsoft.AspNetCore.Diagnostics;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.ResponseCompression;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.AspNetCore.StaticFiles;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.FileProviders;\nusing Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Quic;\nusing System.Net.Security;\nusing System.Reflection;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ClientConnection;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore\n{\n    public sealed partial class DnsWebService : IAsyncDisposable, IDisposable\n    {\n        #region variables\n\n        readonly static char[] commaSeparator = new char[] { ',' };\n\n        readonly Version _currentVersion;\n        readonly DateTime _uptimestamp = DateTime.UtcNow;\n        readonly string _appFolder;\n        readonly string _configFolder;\n\n        readonly LogManager _log;\n        readonly AuthManager _authManager;\n\n        readonly WebServiceApi _api;\n        readonly WebServiceDashboardApi _dashboardApi;\n        readonly WebServiceZonesApi _zonesApi;\n        readonly WebServiceOtherZonesApi _otherZonesApi;\n        readonly WebServiceAppsApi _appsApi;\n        readonly WebServiceSettingsApi _settingsApi;\n        readonly WebServiceDhcpApi _dhcpApi;\n        readonly WebServiceAuthApi _authApi;\n        readonly WebServiceClusterApi _clusterApi;\n        readonly WebServiceLogsApi _logsApi;\n\n        WebApplication _webService;\n\n        ClusterManager _clusterManager;\n        DnsServer _dnsServer;\n        DhcpServer _dhcpServer;\n\n        //web service\n        IReadOnlyList<IPAddress> _webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any];\n        int _webServiceHttpPort = 5380;\n        int _webServiceTlsPort = 53443;\n        bool _webServiceEnableTls;\n        bool _webServiceEnableHttp3;\n        bool _webServiceHttpToTlsRedirect;\n        bool _webServiceUseSelfSignedTlsCertificate;\n        string _webServiceTlsCertificatePath;\n        string _webServiceTlsCertificatePassword;\n        string _webServiceRealIpHeader = \"X-Real-IP\";\n\n        Timer _tlsCertificateUpdateTimer;\n        const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000;\n        const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000;\n\n        DateTime _webServiceCertificateLastModifiedOn;\n        SslServerAuthenticationOptions _webServiceSslServerAuthenticationOptions;\n\n        List<string> _configDisabledZones;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        bool _isRunning;\n\n        #endregion\n\n        #region constructor\n\n        public DnsWebService(string configFolder = null, Uri updateCheckUri = null)\n        {\n            Assembly assembly = Assembly.GetExecutingAssembly();\n\n            _currentVersion = assembly.GetName().Version;\n            _appFolder = Path.GetDirectoryName(assembly.Location);\n\n            if (configFolder is null)\n                _configFolder = Path.Combine(_appFolder, \"config\");\n            else\n                _configFolder = configFolder;\n\n            Directory.CreateDirectory(_configFolder);\n            Directory.CreateDirectory(Path.Combine(_configFolder, \"blocklists\"));\n            Directory.CreateDirectory(Path.Combine(_configFolder, \"zones\"));\n\n            _log = new LogManager(_configFolder);\n            _authManager = new AuthManager(_configFolder, _log);\n\n            _api = new WebServiceApi(this, updateCheckUri);\n            _dashboardApi = new WebServiceDashboardApi(this);\n            _zonesApi = new WebServiceZonesApi(this);\n            _otherZonesApi = new WebServiceOtherZonesApi(this);\n            _appsApi = new WebServiceAppsApi(this);\n            _settingsApi = new WebServiceSettingsApi(this);\n            _dhcpApi = new WebServiceDhcpApi(this);\n            _authApi = new WebServiceAuthApi(this);\n            _clusterApi = new WebServiceClusterApi(this);\n            _logsApi = new WebServiceLogsApi(this);\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveConfigFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public async ValueTask DisposeAsync()\n        {\n            if (_disposed)\n                return;\n\n            StopTlsCertificateUpdateTimer();\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                if (_pendingSave)\n                {\n                    try\n                    {\n                        SaveConfigFileInternal();\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(ex);\n                    }\n                    finally\n                    {\n                        _pendingSave = false;\n                    }\n                }\n            }\n\n            await StopAsync();\n\n            _authManager?.Dispose();\n            _log?.Dispose();\n\n            _disposed = true;\n        }\n\n        public void Dispose()\n        {\n            DisposeAsync().Sync();\n        }\n\n        #endregion\n\n        #region config\n\n        private void LoadConfigFile()\n        {\n            string webServiceConfigFile = Path.Combine(_configFolder, \"webservice.config\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(webServiceConfigFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS);\n                }\n\n                _log.Write(\"Web Service config file was loaded: \" + webServiceConfigFile);\n            }\n            catch (FileNotFoundException)\n            {\n                if (!TryLoadOldConfigFile())\n                {\n                    //old config file did not exist; read environment variables and generate new config\n                    CreateForwarderZoneToDisableDnssecForNTP();\n\n                    //web service\n                    string strWebServiceLocalAddresses = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_LOCAL_ADDRESSES\");\n                    if (!string.IsNullOrEmpty(strWebServiceLocalAddresses))\n                        _webServiceLocalAddresses = strWebServiceLocalAddresses.Split(IPAddress.Parse, commaSeparator);\n\n                    string strWebServiceHttpPort = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_HTTP_PORT\");\n                    if (!string.IsNullOrEmpty(strWebServiceHttpPort))\n                        _webServiceHttpPort = int.Parse(strWebServiceHttpPort);\n\n                    string webServiceTlsPort = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_HTTPS_PORT\");\n                    if (!string.IsNullOrEmpty(webServiceTlsPort))\n                        _webServiceTlsPort = int.Parse(webServiceTlsPort);\n\n                    UdpClientConnection.SocketPoolExcludedPorts = [(ushort)_webServiceTlsPort];\n\n                    string webServiceEnableTls = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS\");\n                    if (!string.IsNullOrEmpty(webServiceEnableTls))\n                        _webServiceEnableTls = bool.Parse(webServiceEnableTls);\n\n                    string webServiceTlsCertificatePassword = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PASSWORD\");\n                    if (!string.IsNullOrEmpty(webServiceTlsCertificatePassword))\n                        _webServiceTlsCertificatePassword = webServiceTlsCertificatePassword;\n\n                    string webServiceTlsCertificatePath = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PATH\");\n                    if (!string.IsNullOrEmpty(webServiceTlsCertificatePath))\n                    {\n                        _webServiceTlsCertificatePath = webServiceTlsCertificatePath;\n\n                        string webServiceTlsCertificateAbsolutePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);\n\n                        try\n                        {\n                            LoadWebServiceTlsCertificate(webServiceTlsCertificateAbsolutePath, _webServiceTlsCertificatePassword);\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(\"DNS Server encountered an error while loading Web Service TLS certificate: \" + webServiceTlsCertificateAbsolutePath + \"\\r\\n\" + ex.ToString());\n                        }\n\n                        StartTlsCertificateUpdateTimer();\n                    }\n\n                    string webServiceUseSelfSignedTlsCertificate = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT\");\n                    if (!string.IsNullOrEmpty(webServiceUseSelfSignedTlsCertificate))\n                    {\n                        _webServiceUseSelfSignedTlsCertificate = bool.Parse(webServiceUseSelfSignedTlsCertificate);\n\n                        if (_webServiceUseSelfSignedTlsCertificate && !File.Exists(Path.Combine(_configFolder, \"dns.config\")))\n                        {\n                            //read DNS server domain name here to generate self signed cert\n                            string serverDomain = Environment.GetEnvironmentVariable(\"DNS_SERVER_DOMAIN\");\n                            if (!string.IsNullOrEmpty(serverDomain))\n                                _dnsServer.ServerDomain = serverDomain;\n                        }\n\n                        CheckAndLoadSelfSignedCertificate(false, false);\n                    }\n\n                    string webServiceHttpToTlsRedirect = Environment.GetEnvironmentVariable(\"DNS_SERVER_WEB_SERVICE_HTTP_TO_TLS_REDIRECT\");\n                    if (!string.IsNullOrEmpty(webServiceHttpToTlsRedirect))\n                        _webServiceHttpToTlsRedirect = bool.Parse(webServiceHttpToTlsRedirect);\n                }\n\n                SaveConfigFileInternal();\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DNS Server encountered an error while loading Web Service config file: \" + webServiceConfigFile + \"\\r\\n\" + ex.ToString());\n                _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.\");\n                throw;\n            }\n        }\n\n        public void LoadConfig(Stream s)\n        {\n            lock (_saveLock)\n            {\n                ReadConfigFrom(s);\n\n                SaveConfigFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void CreateForwarderZoneToDisableDnssecForNTP()\n        {\n            if (Environment.OSVersion.Platform == PlatformID.Unix)\n            {\n                //adding a conditional forwarder zone for disabling DNSSEC validation for ntp.org so that systems with no real-time clock can sync time\n                string ntpDomain = \"ntp.org\";\n                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.\";\n                if (_dnsServer.AuthZoneManager.CreateForwarderZone(ntpDomain, DnsTransportProtocol.Udp, \"this-server\", false, DnsForwarderRecordProxyType.DefaultProxy, null, 0, null, null, fwdRecordComments) is not null)\n                {\n                    //set permissions\n                    _authManager.SetPermission(PermissionSection.Zones, ntpDomain, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                    _authManager.SetPermission(PermissionSection.Zones, ntpDomain, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                    _authManager.SaveConfigFile();\n                }\n            }\n        }\n\n        private void SaveConfigFileInternal()\n        {\n            string configFile = Path.Combine(_configFolder, \"webservice.config\");\n\n            using (MemoryStream mS = new MemoryStream())\n            {\n                //serialize config\n                WriteConfigTo(mS);\n\n                //write config\n                mS.Position = 0;\n\n                using (FileStream fS = new FileStream(configFile, FileMode.Create, FileAccess.Write))\n                {\n                    mS.CopyTo(fS);\n                }\n            }\n\n            _log.Write(\"Web Service config file was saved: \" + configFile);\n        }\n\n        public void SaveConfigFile()\n        {\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void InspectAndFixZonePermissions()\n        {\n            Permission permission = _authManager.GetPermission(PermissionSection.Zones);\n            if (permission is null)\n                throw new DnsWebServiceException(\"Failed to read 'Zones' permissions: auth.config file is probably corrupt.\");\n\n            IReadOnlyDictionary<string, Permission> subItemPermissions = permission.SubItemPermissions;\n\n            //remove ghost permissions\n            foreach (KeyValuePair<string, Permission> subItemPermission in subItemPermissions)\n            {\n                string zoneName = subItemPermission.Key;\n\n                if (_dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName) is null)\n                    permission.RemoveAllSubItemPermissions(zoneName); //no such zone exists; remove permissions\n            }\n\n            //add missing admin permissions\n            IReadOnlyList<AuthZoneInfo> zones = _dnsServer.AuthZoneManager.GetAllZones();\n            Group admins = _authManager.GetGroup(Group.ADMINISTRATORS);\n            if (admins is null)\n                throw new DnsWebServiceException(\"Failed to find 'Administrators' group: auth.config file is probably corrupt.\");\n\n            Group dnsAdmins = _authManager.GetGroup(Group.DNS_ADMINISTRATORS);\n            if (dnsAdmins is null)\n                throw new DnsWebServiceException(\"Failed to find 'DNS Administrators' group: auth.config file is probably corrupt.\");\n\n            foreach (AuthZoneInfo zone in zones)\n            {\n                if (zone.Internal)\n                {\n                    _authManager.SetPermission(PermissionSection.Zones, zone.Name, admins, PermissionFlag.View);\n                    _authManager.SetPermission(PermissionSection.Zones, zone.Name, dnsAdmins, PermissionFlag.View);\n                }\n                else\n                {\n                    _authManager.SetPermission(PermissionSection.Zones, zone.Name, admins, PermissionFlag.ViewModifyDelete);\n                    _authManager.SetPermission(PermissionSection.Zones, zone.Name, dnsAdmins, PermissionFlag.ViewModifyDelete);\n                }\n            }\n\n            _authManager.SaveConfigFile();\n        }\n\n        private void ReadConfigFrom(Stream s)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"WC\") //format\n                throw new InvalidDataException(\"Web Service config file format is invalid.\");\n\n            int version = bR.ReadByte();\n            if (version > 1)\n                throw new InvalidDataException(\"Web Service config version not supported.\");\n\n            _webServiceHttpPort = bR.ReadInt32();\n            _webServiceTlsPort = bR.ReadInt32();\n\n            {\n                IPAddress[] webServiceLocalAddresses;\n\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    IPAddress[] localAddresses = new IPAddress[count];\n\n                    for (int i = 0; i < count; i++)\n                        localAddresses[i] = IPAddressExtensions.ReadFrom(bR);\n\n                    webServiceLocalAddresses = localAddresses;\n                }\n                else\n                {\n                    webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any];\n                }\n\n                _webServiceLocalAddresses = webServiceLocalAddresses;\n            }\n\n            _webServiceEnableTls = bR.ReadBoolean();\n            _webServiceEnableHttp3 = bR.ReadBoolean();\n            _webServiceHttpToTlsRedirect = bR.ReadBoolean();\n            _webServiceUseSelfSignedTlsCertificate = bR.ReadBoolean();\n\n            _webServiceTlsCertificatePath = bR.ReadShortString();\n            _webServiceTlsCertificatePassword = bR.ReadShortString();\n\n            if (_webServiceTlsCertificatePath.Length == 0)\n                _webServiceTlsCertificatePath = null;\n\n            if (_webServiceTlsCertificatePath is null)\n            {\n                StopTlsCertificateUpdateTimer();\n            }\n            else\n            {\n                string webServiceTlsCertificateAbsolutePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);\n\n                try\n                {\n                    LoadWebServiceTlsCertificate(webServiceTlsCertificateAbsolutePath, _webServiceTlsCertificatePassword);\n                }\n                catch (Exception ex)\n                {\n                    _log.Write(\"DNS Server encountered an error while loading Web Service TLS certificate: \" + webServiceTlsCertificateAbsolutePath + \"\\r\\n\" + ex.ToString());\n                }\n\n                StartTlsCertificateUpdateTimer();\n            }\n\n            CheckAndLoadSelfSignedCertificate(false, false);\n\n            _webServiceRealIpHeader = bR.ReadShortString();\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"WC\")); //format\n            bW.Write((byte)1); //version\n\n            bW.Write(_webServiceHttpPort);\n            bW.Write(_webServiceTlsPort);\n\n            {\n                bW.Write(Convert.ToByte(_webServiceLocalAddresses.Count));\n\n                foreach (IPAddress localAddress in _webServiceLocalAddresses)\n                    localAddress.WriteTo(bW);\n            }\n\n            bW.Write(_webServiceEnableTls);\n            bW.Write(_webServiceEnableHttp3);\n            bW.Write(_webServiceHttpToTlsRedirect);\n            bW.Write(_webServiceUseSelfSignedTlsCertificate);\n\n            if (_webServiceTlsCertificatePath is null)\n                bW.WriteShortString(string.Empty);\n            else\n                bW.WriteShortString(_webServiceTlsCertificatePath);\n\n            if (_webServiceTlsCertificatePassword is null)\n                bW.WriteShortString(string.Empty);\n            else\n                bW.WriteShortString(_webServiceTlsCertificatePassword);\n\n            bW.WriteShortString(_webServiceRealIpHeader);\n        }\n\n        #endregion\n\n        #region backup and restore config\n\n        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<string> includeZones = null)\n        {\n            using (ZipArchive backupZip = new ZipArchive(zipStream, ZipArchiveMode.Create, true, Encoding.UTF8))\n            {\n                if (authConfig)\n                {\n                    string authConfigFile = Path.Combine(_configFolder, \"auth.config\");\n\n                    if (File.Exists(authConfigFile) && (File.GetLastWriteTimeUtc(authConfigFile) > ifModifiedSince))\n                        backupZip.CreateEntryFromFile(authConfigFile, \"auth.config\");\n                }\n\n                if (clusterConfig && !isConfigTransfer)\n                {\n                    string clusterConfigFile = Path.Combine(_configFolder, \"cluster.config\");\n\n                    if (File.Exists(clusterConfigFile))\n                        backupZip.CreateEntryFromFile(clusterConfigFile, \"cluster.config\");\n                }\n\n                if (webServiceSettings && !isConfigTransfer)\n                {\n                    string webServiceConfigFile = Path.Combine(_configFolder, \"webservice.config\");\n\n                    if (File.Exists(webServiceConfigFile) && (File.GetLastWriteTimeUtc(webServiceConfigFile) > ifModifiedSince))\n                        backupZip.CreateEntryFromFile(webServiceConfigFile, \"webservice.config\");\n\n                    //backup web service cert\n                    if (!isConfigTransfer && !string.IsNullOrEmpty(_webServiceTlsCertificatePath))\n                    {\n                        string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);\n\n                        if (File.Exists(webServiceTlsCertificatePath) && webServiceTlsCertificatePath.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))\n                        {\n                            string entryName = ConvertToRelativePath(webServiceTlsCertificatePath).Replace('\\\\', '/');\n                            backupZip.CreateEntryFromFile(webServiceTlsCertificatePath, entryName);\n                        }\n                    }\n                }\n\n                if (dnsSettings)\n                {\n                    string dnsConfigFile = Path.Combine(_configFolder, \"dns.config\");\n\n                    if (File.Exists(dnsConfigFile) && (File.GetLastWriteTimeUtc(dnsConfigFile) > ifModifiedSince))\n                        backupZip.CreateEntryFromFile(dnsConfigFile, \"dns.config\");\n\n                    //backup optional protocols cert\n                    if (!isConfigTransfer && !string.IsNullOrEmpty(_dnsServer.DnsTlsCertificatePath))\n                    {\n                        string dnsTlsCertificatePath = ConvertToAbsolutePath(_dnsServer.DnsTlsCertificatePath);\n\n                        if (File.Exists(dnsTlsCertificatePath) && dnsTlsCertificatePath.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))\n                        {\n                            string entryName = ConvertToRelativePath(dnsTlsCertificatePath).Replace('\\\\', '/');\n                            backupZip.CreateEntryFromFile(dnsTlsCertificatePath, entryName);\n                        }\n                    }\n                }\n\n                if (logSettings && !isConfigTransfer)\n                {\n                    string logConfigFile = Path.Combine(_configFolder, \"log.config\");\n\n                    if (File.Exists(logConfigFile) && (File.GetLastWriteTimeUtc(logConfigFile) > ifModifiedSince))\n                        backupZip.CreateEntryFromFile(logConfigFile, \"log.config\");\n                }\n\n                if (zones)\n                {\n                    if (isConfigTransfer)\n                    {\n                        //backup Primary zone DNSSEC private keys that are member zone of the cluster catalog zone\n                        AuthZoneInfo clusterCatalogZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(\"cluster-catalog.\" + _clusterManager.ClusterDomain);\n                        if ((clusterCatalogZoneInfo is not null) && (clusterCatalogZoneInfo.Type == AuthZoneType.Catalog))\n                        {\n                            IReadOnlyCollection<string> memberZoneNames = (clusterCatalogZoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames();\n\n                            foreach (string memberZoneName in memberZoneNames)\n                            {\n                                AuthZoneInfo memberZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName);\n                                if (memberZoneInfo is null)\n                                    continue; //no such zone exists; ignore\n\n                                if (memberZoneInfo.Type != AuthZoneType.Primary)\n                                    continue; //not a Primary zone; ignore\n\n                                if (memberZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                                    continue; //not a DNSSEC signed zone; ignore\n\n                                IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys = memberZoneInfo.DnssecPrivateKeys;\n                                bool includePrivateKeys = false;\n\n                                if ((includeZones is not null) && includeZones.Contains(memberZoneInfo.Name))\n                                {\n                                    includePrivateKeys = true;\n                                }\n                                else\n                                {\n                                    foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)\n                                    {\n                                        if (dnssecPrivateKey.StateChangedOn > ifModifiedSince)\n                                        {\n                                            //found a changed key\n                                            includePrivateKeys = true;\n                                            break;\n                                        }\n                                    }\n                                }\n\n                                if (includePrivateKeys)\n                                {\n                                    using (MemoryStream mS = new MemoryStream(4096))\n                                    {\n                                        AuthZoneInfo.WriteDnssecPrivateKeysTo(dnssecPrivateKeys, new BinaryWriter(mS));\n\n                                        mS.Position = 0;\n\n                                        //create zip entry\n                                        ZipArchiveEntry entry = backupZip.CreateEntry(\"zones/\" + memberZoneName + \".keys\", CompressionLevel.Optimal);\n                                        await using (Stream entryStream = entry.Open())\n                                        {\n                                            await mS.CopyToAsync(entryStream);\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    else\n                    {\n                        //backup zone files\n                        string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, \"zones\"), \"*.zone\", SearchOption.TopDirectoryOnly);\n                        foreach (string zoneFile in zoneFiles)\n                        {\n                            string entryName = \"zones/\" + Path.GetFileName(zoneFile);\n                            backupZip.CreateEntryFromFile(zoneFile, entryName);\n                        }\n                    }\n                }\n\n                if (allowedZones)\n                {\n                    string allowedZonesFile = Path.Combine(_configFolder, \"allowed.config\");\n\n                    if (File.Exists(allowedZonesFile) && (File.GetLastWriteTimeUtc(allowedZonesFile) > ifModifiedSince))\n                        backupZip.CreateEntryFromFile(allowedZonesFile, \"allowed.config\");\n                }\n\n                if (blockedZones)\n                {\n                    string blockedZonesFile = Path.Combine(_configFolder, \"blocked.config\");\n\n                    if (File.Exists(blockedZonesFile) && (File.GetLastWriteTimeUtc(blockedZonesFile) > ifModifiedSince))\n                        backupZip.CreateEntryFromFile(blockedZonesFile, \"blocked.config\");\n                }\n\n                if (blockLists)\n                {\n                    string blockListConfigFile = Path.Combine(_configFolder, \"blocklist.config\");\n\n                    if (File.Exists(blockListConfigFile) && (File.GetLastWriteTimeUtc(blockListConfigFile) > ifModifiedSince))\n                        backupZip.CreateEntryFromFile(blockListConfigFile, \"blocklist.config\");\n\n                    string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, \"blocklists\"), \"*\", SearchOption.TopDirectoryOnly);\n                    foreach (string blockListFile in blockListFiles)\n                    {\n                        if (File.GetLastWriteTimeUtc(blockListFile) > ifModifiedSince)\n                        {\n                            string entryName = \"blocklists/\" + Path.GetFileName(blockListFile);\n                            backupZip.CreateEntryFromFile(blockListFile, entryName);\n                        }\n                    }\n                }\n\n                if (apps)\n                {\n                    if (isConfigTransfer)\n                    {\n                        string[] appDirectories = Directory.GetDirectories(Path.Combine(_configFolder, \"apps\"), \"*\", SearchOption.TopDirectoryOnly);\n                        foreach (string appDirectory in appDirectories)\n                        {\n                            string applicationName = Path.GetFileName(appDirectory);\n                            string applicationZipFile = Path.Combine(appDirectory, applicationName + \".zip\");\n                            string configFile = Path.Combine(appDirectory, \"dnsApp.config\");\n                            bool fileAdded = false;\n\n                            if (File.Exists(applicationZipFile) && (File.GetLastWriteTimeUtc(applicationZipFile) > ifModifiedSince))\n                            {\n                                string entryName = \"apps/\" + applicationName + \"/\" + applicationName + \".zip\";\n                                backupZip.CreateEntryFromFile(applicationZipFile, entryName);\n                                fileAdded = true;\n                            }\n\n                            if (File.Exists(configFile) && (File.GetLastWriteTimeUtc(configFile) > ifModifiedSince))\n                            {\n                                string entryName = \"apps/\" + applicationName + \"/dnsApp.config\";\n                                backupZip.CreateEntryFromFile(configFile, entryName);\n                                fileAdded = true;\n                            }\n\n                            if (!fileAdded)\n                                _ = backupZip.CreateEntry(\"apps/\" + applicationName + \"/.exists\", CompressionLevel.Optimal);\n                        }\n                    }\n                    else\n                    {\n                        string[] appFiles = Directory.GetFiles(Path.Combine(_configFolder, \"apps\"), \"*\", SearchOption.AllDirectories);\n                        foreach (string appFile in appFiles)\n                        {\n                            string entryName = appFile.Substring(_configFolder.Length);\n\n                            if (Path.DirectorySeparatorChar != '/')\n                                entryName = entryName.Replace(Path.DirectorySeparatorChar, '/');\n\n                            entryName = entryName.TrimStart('/');\n\n                            await CreateBackupEntryFromSharedFileAsync(backupZip, appFile, entryName);\n                        }\n                    }\n                }\n\n                if (scopes && !isConfigTransfer)\n                {\n                    string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, \"scopes\"), \"*.scope\", SearchOption.TopDirectoryOnly);\n                    foreach (string scopeFile in scopeFiles)\n                    {\n                        string entryName = \"scopes/\" + Path.GetFileName(scopeFile);\n                        backupZip.CreateEntryFromFile(scopeFile, entryName);\n                    }\n                }\n\n                if (stats && !isConfigTransfer)\n                {\n                    string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, \"stats\"), \"*.stat\", SearchOption.TopDirectoryOnly);\n                    foreach (string hourlyStatsFile in hourlyStatsFiles)\n                    {\n                        string entryName = \"stats/\" + Path.GetFileName(hourlyStatsFile);\n                        backupZip.CreateEntryFromFile(hourlyStatsFile, entryName);\n                    }\n\n                    string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, \"stats\"), \"*.dstat\", SearchOption.TopDirectoryOnly);\n                    foreach (string dailyStatsFile in dailyStatsFiles)\n                    {\n                        string entryName = \"stats/\" + Path.GetFileName(dailyStatsFile);\n                        backupZip.CreateEntryFromFile(dailyStatsFile, entryName);\n                    }\n                }\n\n                if (logs && !isConfigTransfer)\n                {\n                    string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, \"*.log\", SearchOption.TopDirectoryOnly);\n                    foreach (string logFile in logFiles)\n                    {\n                        string entryName = \"logs/\" + Path.GetFileName(logFile);\n\n                        if (logFile.Equals(_log.CurrentLogFile, StringComparison.OrdinalIgnoreCase))\n                        {\n                            await CreateBackupEntryFromSharedFileAsync(backupZip, logFile, entryName);\n                        }\n                        else\n                        {\n                            backupZip.CreateEntryFromFile(logFile, entryName);\n                        }\n                    }\n                }\n            }\n        }\n\n        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)\n        {\n            using (ZipArchive backupZip = new ZipArchive(zipStream, ZipArchiveMode.Read, false, Encoding.UTF8))\n            {\n                if (logSettings && !isConfigTransfer)\n                {\n                    ZipArchiveEntry entry = backupZip.GetEntry(\"log.config\");\n                    if (entry is not null)\n                    {\n                        //dynamically load and apply logger config\n                        await using (Stream stream = entry.Open())\n                        {\n                            _log.LoadConfig(stream);\n                        }\n                    }\n                }\n\n                if (logs && !isConfigTransfer)\n                {\n                    _log.BulkManipulateLogFiles(delegate ()\n                    {\n                        if (deleteExistingFiles)\n                        {\n                            //delete existing log files\n                            string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, \"*.log\", SearchOption.TopDirectoryOnly);\n\n                            foreach (string logFile in logFiles)\n                            {\n                                try\n                                {\n                                    File.Delete(logFile);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n\n                        //extract log files from backup\n                        foreach (ZipArchiveEntry entry in backupZip.Entries)\n                        {\n                            if (entry.FullName.StartsWith(\"logs/\"))\n                            {\n                                try\n                                {\n                                    entry.ExtractToFile(Path.Combine(_log.LogFolderAbsolutePath, entry.Name), true);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n                    });\n                }\n\n                if (authConfig)\n                {\n                    ZipArchiveEntry entry = backupZip.GetEntry(\"auth.config\");\n                    if (entry is not null)\n                    {\n                        //dynamically load and apply auth config\n                        await using (Stream stream = entry.Open())\n                        {\n                            _authManager.LoadConfig(stream, isConfigTransfer, implantSession);\n                        }\n                    }\n                }\n\n                if (clusterConfig && !isConfigTransfer)\n                {\n                    ZipArchiveEntry entry = backupZip.GetEntry(\"cluster.config\");\n                    if (entry is not null)\n                    {\n                        //dynamically load and apply cluster config\n                        await using (Stream stream = entry.Open())\n                        {\n                            _clusterManager.LoadConfig(stream);\n                        }\n                    }\n                }\n\n                if ((webServiceSettings || dnsSettings) && !isConfigTransfer)\n                {\n                    //extract any certs\n                    foreach (ZipArchiveEntry certEntry in backupZip.Entries)\n                    {\n                        if (certEntry.FullName.StartsWith(\"apps/\"))\n                            continue;\n\n                        if (certEntry.FullName.EndsWith(\".pfx\", StringComparison.OrdinalIgnoreCase) || certEntry.FullName.EndsWith(\".p12\", StringComparison.OrdinalIgnoreCase))\n                        {\n                            string certFile = Path.Combine(_configFolder, certEntry.FullName);\n\n                            try\n                            {\n                                Directory.CreateDirectory(Path.GetDirectoryName(certFile));\n\n                                certEntry.ExtractToFile(certFile, true);\n                            }\n                            catch (Exception ex)\n                            {\n                                _log.Write(ex);\n                            }\n                        }\n                    }\n                }\n\n                if (webServiceSettings && !isConfigTransfer)\n                {\n                    ZipArchiveEntry entry = backupZip.GetEntry(\"webservice.config\");\n                    if (entry is not null)\n                    {\n                        //dynamically load and apply web service config\n                        await using (Stream stream = entry.Open())\n                        {\n                            LoadConfig(stream);\n                        }\n                    }\n                }\n\n                if (dnsSettings)\n                {\n                    ZipArchiveEntry entry = backupZip.GetEntry(\"dns.config\");\n                    if (entry is not null)\n                    {\n                        try\n                        {\n                            //dynamically load and apply DNS settings config\n                            await using (Stream stream = entry.Open())\n                            {\n                                _dnsServer.LoadConfig(stream, isConfigTransfer);\n                            }\n                        }\n                        catch (InvalidDataException)\n                        {\n                            if (isConfigTransfer)\n                                throw; //config being synced; throw same exception\n\n                            //most probably an attempt to restore old config\n                            await using (Stream stream = entry.Open())\n                            {\n                                if (!TryLoadOldConfigFrom(stream))\n                                    throw; //was not old config file so must be corrupt config file; throw same exception\n\n                                _log.Write(\"Old DNS config file was restored successfully.\");\n\n                                //explicitly save webservice.config\n                                SaveConfigFileInternal();\n                            }\n                        }\n                    }\n                }\n\n                if (zones)\n                {\n                    if (isConfigTransfer)\n                    {\n                        //backup DNSSEC private keys into Secondary zones that are member zone of the secondary cluster catalog zone\n                        AuthZoneInfo secondaryClusterCatalogZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(\"cluster-catalog.\" + _clusterManager.ClusterDomain);\n                        if ((secondaryClusterCatalogZoneInfo is not null) && (secondaryClusterCatalogZoneInfo.Type == AuthZoneType.SecondaryCatalog))\n                        {\n                            HashSet<string> memberZoneNames = new HashSet<string>((secondaryClusterCatalogZoneInfo.ApexZone as SecondaryCatalogZone).GetAllMemberZoneNames());\n\n                            foreach (ZipArchiveEntry entry in backupZip.Entries)\n                            {\n                                if (!entry.FullName.StartsWith(\"zones/\") || !entry.FullName.EndsWith(\".keys\", StringComparison.Ordinal))\n                                    continue;\n\n                                string memberZoneName = Path.GetFileNameWithoutExtension(entry.Name);\n\n                                AuthZoneInfo memberZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName);\n                                if (memberZoneInfo is null)\n                                    continue; //no such zone exists; ignore\n\n                                if (memberZoneInfo.Type != AuthZoneType.Secondary)\n                                    continue; //not a Secondary zone; ignore\n\n                                SecondaryZone memberZone = memberZoneInfo.ApexZone as SecondaryZone;\n\n                                if (memberZoneNames.Contains(memberZoneName))\n                                {\n                                    //read DNSSEC private keys\n                                    IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys;\n\n                                    await using (Stream s = entry.Open())\n                                    {\n                                        dnssecPrivateKeys = AuthZoneInfo.ReadDnssecPrivateKeysFrom(new BinaryReader(s));\n                                    }\n\n                                    //backup DNSSEC private keys\n                                    memberZone.DnssecPrivateKeys = dnssecPrivateKeys;\n                                    _dnsServer.AuthZoneManager.SaveZoneFile(memberZoneInfo.Name);\n                                }\n                                else\n                                {\n                                    //not a member zone of the secondary cluster catalog zone\n                                    if (memberZone.DnssecPrivateKeys is not null)\n                                    {\n                                        //found old backup keys; remove them\n                                        memberZone.DnssecPrivateKeys = null;\n                                        _dnsServer.AuthZoneManager.SaveZoneFile(memberZoneInfo.Name);\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    else\n                    {\n                        //restore zones\n                        if (deleteExistingFiles)\n                        {\n                            //delete existing zone files\n                            string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, \"zones\"), \"*.zone\", SearchOption.TopDirectoryOnly);\n\n                            foreach (string zoneFile in zoneFiles)\n                            {\n                                try\n                                {\n                                    File.Delete(zoneFile);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n\n                        //extract zone files from backup\n                        foreach (ZipArchiveEntry entry in backupZip.Entries)\n                        {\n                            if (entry.FullName.StartsWith(\"zones/\"))\n                            {\n                                try\n                                {\n                                    entry.ExtractToFile(Path.Combine(_configFolder, \"zones\", entry.Name), true);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n\n                        //reload zones\n                        _dnsServer.AuthZoneManager.LoadAllZoneFiles();\n                        InspectAndFixZonePermissions();\n                    }\n                }\n\n                if (allowedZones)\n                {\n                    ZipArchiveEntry entry = backupZip.GetEntry(\"allowed.config\");\n                    if (entry is not null)\n                    {\n                        //dynamically load and apply allowed zones config\n                        await using (Stream stream = entry.Open())\n                        {\n                            _dnsServer.AllowedZoneManager.LoadAllowedZone(stream);\n                        }\n                    }\n                }\n\n                if (blockedZones)\n                {\n                    ZipArchiveEntry entry = backupZip.GetEntry(\"blocked.config\");\n                    if (entry is not null)\n                    {\n                        //dynamically load and apply blocked zones config\n                        await using (Stream stream = entry.Open())\n                        {\n                            _dnsServer.BlockedZoneManager.LoadBlockedZone(stream);\n                        }\n                    }\n                }\n\n                if (blockLists)\n                {\n                    if (deleteExistingFiles)\n                    {\n                        //delete existing block list files\n                        string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, \"blocklists\"), \"*\", SearchOption.TopDirectoryOnly);\n\n                        foreach (string blockListFile in blockListFiles)\n                        {\n                            try\n                            {\n                                File.Delete(blockListFile);\n                            }\n                            catch (Exception ex)\n                            {\n                                _log.Write(ex);\n                            }\n                        }\n                    }\n\n                    //extract block list files from backup\n                    foreach (ZipArchiveEntry entry in backupZip.Entries)\n                    {\n                        if (entry.FullName.StartsWith(\"blocklists/\"))\n                        {\n                            try\n                            {\n                                entry.ExtractToFile(Path.Combine(_configFolder, \"blocklists\", entry.Name), true);\n                            }\n                            catch (IOException)\n                            {\n                                //ignore since file may be loading in another thread\n                            }\n                            catch (Exception ex)\n                            {\n                                _log.Write(ex);\n                            }\n                        }\n                    }\n\n                    ZipArchiveEntry blockListConfigEntry = backupZip.GetEntry(\"blocklist.config\");\n                    if (blockListConfigEntry is not null)\n                    {\n                        //dynamically load and apply block list config\n                        await using (Stream stream = blockListConfigEntry.Open())\n                        {\n                            _dnsServer.BlockListZoneManager.LoadConfig(stream, isConfigTransfer);\n                        }\n                    }\n                }\n\n                if (apps)\n                {\n                    if (isConfigTransfer)\n                    {\n                        //install or update app from zip\n                        foreach (ZipArchiveEntry entry in backupZip.Entries)\n                        {\n                            if (!entry.FullName.StartsWith(\"apps/\"))\n                                continue;\n\n                            string[] fullNameParts = entry.FullName.Split('/');\n                            if (fullNameParts.Length < 3)\n                                continue;\n\n                            string applicationName = fullNameParts[1];\n                            string applicationZipFile = fullNameParts[2];\n\n                            if (!applicationZipFile.Equals(applicationName + \".zip\", StringComparison.Ordinal))\n                                continue;\n\n                            if (_dnsServer.DnsApplicationManager.Applications.TryGetValue(applicationName, out _))\n                            {\n                                //update existing app\n                                await using (Stream s = entry.Open())\n                                {\n                                    await _dnsServer.DnsApplicationManager.UpdateApplicationAsync(applicationName, s);\n                                }\n                            }\n                            else\n                            {\n                                //install new app\n                                await using (Stream s = entry.Open())\n                                {\n                                    await _dnsServer.DnsApplicationManager.InstallApplicationAsync(applicationName, s);\n                                }\n                            }\n                        }\n\n                        //update app config\n                        foreach (ZipArchiveEntry entry in backupZip.Entries)\n                        {\n                            if (!entry.FullName.StartsWith(\"apps/\"))\n                                continue;\n\n                            string[] fullNameParts = entry.FullName.Split('/');\n                            if (fullNameParts.Length < 3)\n                                continue;\n\n                            string applicationName = fullNameParts[1];\n                            string configFile = fullNameParts[2];\n\n                            if (!configFile.Equals(\"dnsApp.config\", StringComparison.Ordinal))\n                                continue;\n\n                            if (_dnsServer.DnsApplicationManager.Applications.TryGetValue(applicationName, out DnsApplication application))\n                            {\n                                string config;\n\n                                await using (Stream s = entry.Open())\n                                {\n                                    using (StreamReader sR = new StreamReader(s, true))\n                                    {\n                                        config = await sR.ReadToEndAsync();\n                                    }\n                                }\n\n                                try\n                                {\n                                    await application.SetConfigAsync(config);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n\n                        //remove apps that are not in the zip file\n                        HashSet<string> existingApplications = new HashSet<string>();\n\n                        foreach (ZipArchiveEntry entry in backupZip.Entries)\n                        {\n                            if (!entry.FullName.StartsWith(\"apps/\"))\n                                continue;\n\n                            string[] fullNameParts = entry.FullName.Split('/');\n                            if (fullNameParts.Length < 2)\n                                continue;\n\n                            string applicationName = fullNameParts[1];\n\n                            existingApplications.Add(applicationName);\n                        }\n\n                        foreach (KeyValuePair<string, DnsApplication> application in _dnsServer.DnsApplicationManager.Applications)\n                        {\n                            if (!existingApplications.Contains(application.Key))\n                                _dnsServer.DnsApplicationManager.UninstallApplication(application.Key);\n                        }\n                    }\n                    else\n                    {\n                        //unload apps\n                        _dnsServer.DnsApplicationManager.UnloadAllApplications();\n\n                        if (deleteExistingFiles)\n                        {\n                            //delete existing apps\n                            string appFolder = Path.Combine(_configFolder, \"apps\");\n                            if (Directory.Exists(appFolder))\n                            {\n                                try\n                                {\n                                    Directory.Delete(appFolder, true);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n\n                            //create apps folder\n                            Directory.CreateDirectory(appFolder);\n                        }\n\n                        //extract apps files from backup\n                        foreach (ZipArchiveEntry entry in backupZip.Entries)\n                        {\n                            if (entry.FullName.StartsWith(\"apps/\"))\n                            {\n                                string entryPath = entry.FullName;\n\n                                if (Path.DirectorySeparatorChar != '/')\n                                    entryPath = entryPath.Replace('/', '\\\\');\n\n                                string filePath = Path.Combine(_configFolder, entryPath);\n\n                                Directory.CreateDirectory(Path.GetDirectoryName(filePath));\n\n                                try\n                                {\n                                    entry.ExtractToFile(filePath, true);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n\n                        //reload apps\n                        await _dnsServer.DnsApplicationManager.LoadAllApplicationsAsync();\n                    }\n                }\n\n                if (scopes && !isConfigTransfer)\n                {\n                    //stop dhcp server\n                    _dhcpServer.Stop();\n\n                    try\n                    {\n                        if (deleteExistingFiles)\n                        {\n                            //delete existing scope files\n                            string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, \"scopes\"), \"*.scope\", SearchOption.TopDirectoryOnly);\n\n                            foreach (string scopeFile in scopeFiles)\n                            {\n                                try\n                                {\n                                    File.Delete(scopeFile);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n\n                        //extract scope files from backup\n                        foreach (ZipArchiveEntry entry in backupZip.Entries)\n                        {\n                            if (entry.FullName.StartsWith(\"scopes/\"))\n                            {\n                                try\n                                {\n                                    entry.ExtractToFile(Path.Combine(_configFolder, \"scopes\", entry.Name), true);\n                                }\n                                catch (Exception ex)\n                                {\n                                    _log.Write(ex);\n                                }\n                            }\n                        }\n                    }\n                    finally\n                    {\n                        //start dhcp server\n                        _dhcpServer.Start();\n                    }\n                }\n\n                if (stats && !isConfigTransfer)\n                {\n                    if (deleteExistingFiles)\n                    {\n                        //delete existing stats files\n                        string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, \"stats\"), \"*.stat\", SearchOption.TopDirectoryOnly);\n\n                        foreach (string hourlyStatsFile in hourlyStatsFiles)\n                        {\n                            try\n                            {\n                                File.Delete(hourlyStatsFile);\n                            }\n                            catch (Exception ex)\n                            {\n                                _log.Write(ex);\n                            }\n                        }\n\n                        string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, \"stats\"), \"*.dstat\", SearchOption.TopDirectoryOnly);\n\n                        foreach (string dailyStatsFile in dailyStatsFiles)\n                        {\n                            try\n                            {\n                                File.Delete(dailyStatsFile);\n                            }\n                            catch (Exception ex)\n                            {\n                                _log.Write(ex);\n                            }\n                        }\n                    }\n\n                    //extract stats files from backup\n                    foreach (ZipArchiveEntry entry in backupZip.Entries)\n                    {\n                        if (entry.FullName.StartsWith(\"stats/\"))\n                        {\n                            try\n                            {\n                                entry.ExtractToFile(Path.Combine(_configFolder, \"stats\", entry.Name), true);\n                            }\n                            catch (Exception ex)\n                            {\n                                _log.Write(ex);\n                            }\n                        }\n                    }\n\n                    //reload stats\n                    _dnsServer.StatsManager.ReloadStats();\n                }\n            }\n        }\n\n        private static async Task CreateBackupEntryFromSharedFileAsync(ZipArchive backupZip, string sourceFileName, string entryName)\n        {\n            await using (FileStream fS = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))\n            {\n                ZipArchiveEntry entry = backupZip.CreateEntry(entryName);\n\n                DateTime lastWrite = File.GetLastWriteTime(sourceFileName);\n\n                // If file to be archived has an invalid last modified time, use the first datetime representable in the Zip timestamp format\n                // (midnight on January 1, 1980):\n                if (lastWrite.Year < 1980 || lastWrite.Year > 2107)\n                    lastWrite = new DateTime(1980, 1, 1, 0, 0, 0);\n\n                entry.LastWriteTime = lastWrite;\n\n                await using (Stream sE = entry.Open())\n                {\n                    await fS.CopyToAsync(sE);\n                }\n            }\n        }\n\n        #endregion\n\n        #region internal\n\n        private string ConvertToRelativePath(string path)\n        {\n            if (path.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))\n                path = path.Substring(_configFolder.Length).TrimStart(Path.DirectorySeparatorChar);\n\n            return path;\n        }\n\n        private string ConvertToAbsolutePath(string path)\n        {\n            if (path is null)\n                return null;\n\n            if (Path.IsPathRooted(path))\n                return path;\n\n            return Path.Combine(_configFolder, path);\n        }\n\n        #endregion\n\n        #region server version\n\n        private string GetServerVersion()\n        {\n            return GetCleanVersion(_currentVersion);\n        }\n\n        private static string GetCleanVersion(Version version)\n        {\n            string strVersion = version.Major + \".\" + version.Minor;\n\n            if (version.Build > 0)\n                strVersion += \".\" + version.Build;\n\n            if (version.Revision > 0)\n                strVersion += \".\" + version.Revision;\n\n            return strVersion;\n        }\n\n        #endregion\n\n        #region web service\n\n        private async Task TryStartWebServiceAsync(IReadOnlyList<IPAddress> oldWebServiceLocalAddresses, int oldWebServiceHttpPort, int oldWebServiceTlsPort)\n        {\n            try\n            {\n                _webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(_webServiceLocalAddresses);\n\n                await StartWebServiceAsync(false);\n                return;\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"Web Service failed to start: \" + ex.ToString());\n            }\n\n            _log.Write(\"Attempting to revert Web Service end point changes ...\");\n\n            try\n            {\n                _webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(oldWebServiceLocalAddresses);\n                _webServiceHttpPort = oldWebServiceHttpPort;\n                _webServiceTlsPort = oldWebServiceTlsPort;\n\n                await StartWebServiceAsync(false);\n\n                SaveConfigFileInternal(); //save reverted changes\n                return;\n            }\n            catch (Exception ex2)\n            {\n                _log.Write(\"Web Service failed to start: \" + ex2.ToString());\n            }\n\n            _log.Write(\"Attempting to start Web Service on ANY (0.0.0.0) fallback address...\");\n\n            try\n            {\n                _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any };\n\n                await StartWebServiceAsync(true);\n                return;\n            }\n            catch (Exception ex3)\n            {\n                _log.Write(\"Web Service failed to start: \" + ex3.ToString());\n            }\n\n            _log.Write(\"Attempting to start Web Service on loopback (127.0.0.1) fallback address...\");\n\n            _webServiceLocalAddresses = new IPAddress[] { IPAddress.Loopback };\n\n            await StartWebServiceAsync(true);\n        }\n\n        private async Task StartWebServiceAsync(bool httpOnlyMode)\n        {\n            WebApplicationBuilder builder = WebApplication.CreateBuilder();\n\n            builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(_appFolder)\n            {\n                UseActivePolling = true,\n                UsePollingFileWatcher = true\n            };\n\n            builder.Environment.WebRootFileProvider = new PhysicalFileProvider(Path.Combine(_appFolder, \"www\"))\n            {\n                UseActivePolling = true,\n                UsePollingFileWatcher = true\n            };\n\n            builder.Services.AddResponseCompression(delegate (ResponseCompressionOptions options)\n            {\n                options.EnableForHttps = true;\n            });\n\n            builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions)\n            {\n                //http\n                foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)\n                    serverOptions.Listen(webServiceLocalAddress, _webServiceHttpPort);\n\n                //https\n                if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))\n                {\n                    foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)\n                    {\n                        serverOptions.Listen(webServiceLocalAddress, _webServiceTlsPort, delegate (ListenOptions listenOptions)\n                        {\n                            if (_webServiceEnableHttp3)\n                                listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;\n                            else if (IsHttp2Supported())\n                                listenOptions.Protocols = HttpProtocols.Http1AndHttp2;\n                            else\n                                listenOptions.Protocols = HttpProtocols.Http1;\n\n                            listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)\n                            {\n                                return ValueTask.FromResult(_webServiceSslServerAuthenticationOptions);\n                            }, null);\n                        });\n                    }\n                }\n\n                serverOptions.AddServerHeader = false;\n                serverOptions.Limits.MaxRequestBodySize = int.MaxValue;\n            });\n\n            builder.Services.Configure(delegate (FormOptions options)\n            {\n                options.MultipartBodyLengthLimit = int.MaxValue;\n            });\n\n            builder.Logging.ClearProviders();\n\n            _webService = builder.Build();\n\n            _webService.UseResponseCompression();\n\n            if (_webServiceHttpToTlsRedirect && !httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))\n                _webService.Use(WebServiceHttpsRedirectionMiddleware);\n\n            _webService.UseDefaultFiles();\n            _webService.UseStaticFiles(new StaticFileOptions()\n            {\n                OnPrepareResponse = delegate (StaticFileResponseContext ctx)\n                {\n                    ctx.Context.Response.Headers[\"X-Robots-Tag\"] = \"noindex, nofollow\";\n                    ctx.Context.Response.Headers.CacheControl = \"no-cache\";\n                },\n                ServeUnknownFileTypes = true\n            });\n\n            ConfigureWebServiceRoutes();\n\n            try\n            {\n                await _webService.StartAsync();\n\n                foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)\n                {\n                    _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), \"Http\", \"Web Service was bound successfully.\");\n\n                    if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))\n                        _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceTlsPort), \"Https\", \"Web Service was bound successfully.\");\n                }\n            }\n            catch\n            {\n                await StopWebServiceAsync();\n\n                foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)\n                {\n                    _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), \"Http\", \"Web Service failed to bind.\");\n\n                    if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))\n                        _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceTlsPort), \"Https\", \"Web Service failed to bind.\");\n                }\n\n                throw;\n            }\n        }\n\n        private async Task StopWebServiceAsync()\n        {\n            if (_webService is not null)\n            {\n                await _webService.DisposeAsync();\n                _webService = null;\n            }\n        }\n\n        private bool IsHttp2Supported()\n        {\n            if (_webServiceEnableHttp3)\n                return true;\n\n            switch (Environment.OSVersion.Platform)\n            {\n                case PlatformID.Win32NT:\n                    return Environment.OSVersion.Version.Major >= 10; //http/2 supported on Windows Server 2016/Windows 10 or later\n\n                case PlatformID.Unix:\n                    return true; //http/2 supported on Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)\n\n                default:\n                    return false;\n            }\n        }\n\n        private void ConfigureWebServiceRoutes()\n        {\n            _webService.UseExceptionHandler(WebServiceExceptionHandler);\n\n            _webService.Use(WebServiceApiMiddleware);\n\n            _webService.UseRouting();\n\n            //user auth\n            _webService.MapGetAndPost(\"/api/user/login\", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.Standard); });\n            _webService.MapGetAndPost(\"/api/user/createToken\", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.ApiToken); });\n            _webService.MapGetAndPost(\"/api/user/logout\", _authApi.Logout);\n\n            //user\n            _webService.MapGetAndPost(\"/api/user/session/get\", _authApi.GetCurrentSessionDetails);\n            _webService.MapGetAndPost(\"/api/user/session/delete\", delegate (HttpContext context) { _authApi.DeleteSession(context, false); });\n            _webService.MapGetAndPost(\"/api/user/changePassword\", _authApi.ChangePasswordAsync);\n            _webService.MapGetAndPost(\"/api/user/2fa/init\", _authApi.Initialize2FA);\n            _webService.MapGetAndPost(\"/api/user/2fa/enable\", _authApi.Enable2FA);\n            _webService.MapGetAndPost(\"/api/user/2fa/disable\", _authApi.Disable2FA);\n            _webService.MapGetAndPost(\"/api/user/profile/get\", _authApi.GetProfile);\n            _webService.MapGetAndPost(\"/api/user/profile/set\", _authApi.SetProfile);\n            _webService.MapGetAndPost(\"/api/user/checkForUpdate\", _api.CheckForUpdateAsync);\n\n            //dashboard\n            _webService.MapGetAndPost(\"/api/dashboard/stats/get\", _dashboardApi.GetStats);\n            _webService.MapGetAndPost(\"/api/dashboard/stats/getTop\", _dashboardApi.GetTopStats);\n            _webService.MapGetAndPost(\"/api/dashboard/stats/deleteAll\", _logsApi.DeleteAllStats);\n\n            //zones\n            _webService.MapGetAndPost(\"/api/zones/list\", _zonesApi.ListZones);\n            _webService.MapGetAndPost(\"/api/zones/catalogs/list\", _zonesApi.ListCatalogZones);\n            _webService.MapGetAndPost(\"/api/zones/create\", _zonesApi.CreateZoneAsync);\n            _webService.MapGetAndPost(\"/api/zones/import\", _zonesApi.ImportZoneAsync);\n            _webService.MapGetAndPost(\"/api/zones/export\", _zonesApi.ExportZoneAsync);\n            _webService.MapGetAndPost(\"/api/zones/clone\", _zonesApi.CloneZone);\n            _webService.MapGetAndPost(\"/api/zones/convert\", _zonesApi.ConvertZone);\n            _webService.MapGetAndPost(\"/api/zones/enable\", _zonesApi.EnableZone);\n            _webService.MapGetAndPost(\"/api/zones/disable\", _zonesApi.DisableZone);\n            _webService.MapGetAndPost(\"/api/zones/delete\", _zonesApi.DeleteZone);\n            _webService.MapGetAndPost(\"/api/zones/resync\", _zonesApi.ResyncZone);\n            _webService.MapGetAndPost(\"/api/zones/options/get\", _zonesApi.GetZoneOptions);\n            _webService.MapGetAndPost(\"/api/zones/options/set\", _zonesApi.SetZoneOptions);\n            _webService.MapGetAndPost(\"/api/zones/permissions/get\", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Zones); });\n            _webService.MapGetAndPost(\"/api/zones/permissions/set\", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Zones); });\n            _webService.MapGetAndPost(\"/api/zones/dnssec/sign\", _zonesApi.SignPrimaryZone);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/unsign\", _zonesApi.UnsignPrimaryZone);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/viewDS\", _zonesApi.GetPrimaryZoneDsInfo);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/get\", _zonesApi.GetPrimaryZoneDnssecProperties);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/convertToNSEC\", _zonesApi.ConvertPrimaryZoneToNSEC);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/convertToNSEC3\", _zonesApi.ConvertPrimaryZoneToNSEC3);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/updateNSEC3Params\", _zonesApi.UpdatePrimaryZoneNSEC3Parameters);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/updateDnsKeyTtl\", _zonesApi.UpdatePrimaryZoneDnssecDnsKeyTtl);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/generatePrivateKey\", _zonesApi.AddPrimaryZoneDnssecPrivateKey);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/addPrivateKey\", _zonesApi.AddPrimaryZoneDnssecPrivateKey);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/updatePrivateKey\", _zonesApi.UpdatePrimaryZoneDnssecPrivateKey);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/deletePrivateKey\", _zonesApi.DeletePrimaryZoneDnssecPrivateKey);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/publishAllPrivateKeys\", _zonesApi.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/rolloverDnsKey\", _zonesApi.RolloverPrimaryZoneDnsKey);\n            _webService.MapGetAndPost(\"/api/zones/dnssec/properties/retireDnsKey\", _zonesApi.RetirePrimaryZoneDnsKeyAsync);\n            _webService.MapGetAndPost(\"/api/zones/records/add\", _zonesApi.AddRecord);\n            _webService.MapGetAndPost(\"/api/zones/records/get\", _zonesApi.GetRecords);\n            _webService.MapGetAndPost(\"/api/zones/records/update\", _zonesApi.UpdateRecord);\n            _webService.MapGetAndPost(\"/api/zones/records/delete\", _zonesApi.DeleteRecord);\n\n            //cache\n            _webService.MapGetAndPost(\"/api/cache/list\", _otherZonesApi.ListCachedZones);\n            _webService.MapGetAndPost(\"/api/cache/delete\", _otherZonesApi.DeleteCachedZone);\n            _webService.MapGetAndPost(\"/api/cache/flush\", _otherZonesApi.FlushCache);\n\n            //allowed\n            _webService.MapGetAndPost(\"/api/allowed/list\", _otherZonesApi.ListAllowedZones);\n            _webService.MapGetAndPost(\"/api/allowed/add\", _otherZonesApi.AllowZone);\n            _webService.MapGetAndPost(\"/api/allowed/delete\", _otherZonesApi.DeleteAllowedZone);\n            _webService.MapGetAndPost(\"/api/allowed/flush\", _otherZonesApi.FlushAllowedZone);\n            _webService.MapGetAndPost(\"/api/allowed/import\", _otherZonesApi.ImportAllowedZones);\n            _webService.MapGetAndPost(\"/api/allowed/export\", _otherZonesApi.ExportAllowedZonesAsync);\n\n            //blocked\n            _webService.MapGetAndPost(\"/api/blocked/list\", _otherZonesApi.ListBlockedZones);\n            _webService.MapGetAndPost(\"/api/blocked/add\", _otherZonesApi.BlockZone);\n            _webService.MapGetAndPost(\"/api/blocked/delete\", _otherZonesApi.DeleteBlockedZone);\n            _webService.MapGetAndPost(\"/api/blocked/flush\", _otherZonesApi.FlushBlockedZone);\n            _webService.MapGetAndPost(\"/api/blocked/import\", _otherZonesApi.ImportBlockedZones);\n            _webService.MapGetAndPost(\"/api/blocked/export\", _otherZonesApi.ExportBlockedZonesAsync);\n\n            //apps\n            _webService.MapGetAndPost(\"/api/apps/list\", _appsApi.ListInstalledAppsAsync);\n            _webService.MapGetAndPost(\"/api/apps/listStoreApps\", _appsApi.ListStoreApps);\n            _webService.MapGetAndPost(\"/api/apps/downloadAndInstall\", _appsApi.DownloadAndInstallAppAsync);\n            _webService.MapGetAndPost(\"/api/apps/downloadAndUpdate\", _appsApi.DownloadAndUpdateAppAsync);\n            _webService.MapPost(\"/api/apps/install\", _appsApi.InstallAppAsync);\n            _webService.MapPost(\"/api/apps/update\", _appsApi.UpdateAppAsync);\n            _webService.MapGetAndPost(\"/api/apps/uninstall\", _appsApi.UninstallApp);\n            _webService.MapGetAndPost(\"/api/apps/config/get\", _appsApi.GetAppConfigAsync);\n            _webService.MapGetAndPost(\"/api/apps/config/set\", _appsApi.SetAppConfigAsync);\n\n            //dns client\n            _webService.MapGetAndPost(\"/api/dnsClient/resolve\", _api.ResolveQueryAsync);\n\n            //settings\n            _webService.MapGetAndPost(\"/api/settings/get\", _settingsApi.GetDnsSettings);\n            _webService.MapGetAndPost(\"/api/settings/set\", _settingsApi.SetDnsSettingsAsync);\n            _webService.MapGetAndPost(\"/api/settings/getTsigKeyNames\", _settingsApi.GetTsigKeyNames);\n            _webService.MapGetAndPost(\"/api/settings/forceUpdateBlockLists\", _settingsApi.ForceUpdateBlockLists);\n            _webService.MapGetAndPost(\"/api/settings/temporaryDisableBlocking\", _settingsApi.TemporaryDisableBlocking);\n            _webService.MapGetAndPost(\"/api/settings/backup\", _settingsApi.BackupSettingsAsync);\n            _webService.MapPost(\"/api/settings/restore\", _settingsApi.RestoreSettingsAsync);\n\n            //dhcp\n            _webService.MapGetAndPost(\"/api/dhcp/leases/list\", _dhcpApi.ListDhcpLeases);\n            _webService.MapGetAndPost(\"/api/dhcp/leases/remove\", _dhcpApi.RemoveDhcpLease);\n            _webService.MapGetAndPost(\"/api/dhcp/leases/convertToReserved\", _dhcpApi.ConvertToReservedLease);\n            _webService.MapGetAndPost(\"/api/dhcp/leases/convertToDynamic\", _dhcpApi.ConvertToDynamicLease);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/list\", _dhcpApi.ListDhcpScopes);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/get\", _dhcpApi.GetDhcpScope);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/set\", _dhcpApi.SetDhcpScopeAsync);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/addReservedLease\", _dhcpApi.AddReservedLease);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/removeReservedLease\", _dhcpApi.RemoveReservedLease);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/enable\", _dhcpApi.EnableDhcpScopeAsync);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/disable\", _dhcpApi.DisableDhcpScope);\n            _webService.MapGetAndPost(\"/api/dhcp/scopes/delete\", _dhcpApi.DeleteDhcpScope);\n\n            //administration\n            _webService.MapGetAndPost(\"/api/admin/sessions/list\", _authApi.ListSessions);\n            _webService.MapGetAndPost(\"/api/admin/sessions/createToken\", _authApi.CreateApiToken);\n            _webService.MapGetAndPost(\"/api/admin/sessions/delete\", delegate (HttpContext context) { _authApi.DeleteSession(context, true); });\n            _webService.MapGetAndPost(\"/api/admin/users/list\", _authApi.ListUsers);\n            _webService.MapGetAndPost(\"/api/admin/users/create\", _authApi.CreateUser);\n            _webService.MapGetAndPost(\"/api/admin/users/get\", _authApi.GetUserDetails);\n            _webService.MapGetAndPost(\"/api/admin/users/set\", _authApi.SetUserDetails);\n            _webService.MapGetAndPost(\"/api/admin/users/delete\", _authApi.DeleteUser);\n            _webService.MapGetAndPost(\"/api/admin/groups/list\", _authApi.ListGroups);\n            _webService.MapGetAndPost(\"/api/admin/groups/create\", _authApi.CreateGroup);\n            _webService.MapGetAndPost(\"/api/admin/groups/get\", _authApi.GetGroupDetails);\n            _webService.MapGetAndPost(\"/api/admin/groups/set\", _authApi.SetGroupDetails);\n            _webService.MapGetAndPost(\"/api/admin/groups/delete\", _authApi.DeleteGroup);\n            _webService.MapGetAndPost(\"/api/admin/permissions/list\", _authApi.ListPermissions);\n            _webService.MapGetAndPost(\"/api/admin/permissions/get\", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Unknown); });\n            _webService.MapGetAndPost(\"/api/admin/permissions/set\", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Unknown); });\n            _webService.MapGetAndPost(\"/api/admin/cluster/state\", _clusterApi.GetClusterState);\n            _webService.MapGetAndPost(\"/api/admin/cluster/init\", _clusterApi.InitializeCluster);\n            _webService.MapGetAndPost(\"/api/admin/cluster/primary/delete\", _clusterApi.DeleteCluster);\n            _webService.MapGetAndPost(\"/api/admin/cluster/primary/join\", _clusterApi.JoinCluster);\n            _webService.MapGetAndPost(\"/api/admin/cluster/primary/removeSecondary\", _clusterApi.RemoveSecondaryNodeAsync);\n            _webService.MapGetAndPost(\"/api/admin/cluster/primary/deleteSecondary\", _clusterApi.DeleteSecondaryNode);\n            _webService.MapGetAndPost(\"/api/admin/cluster/primary/updateSecondary\", _clusterApi.UpdateSecondaryNode);\n            _webService.MapGetAndPost(\"/api/admin/cluster/primary/transferConfig\", _clusterApi.TransferConfigAsync);\n            _webService.MapGetAndPost(\"/api/admin/cluster/primary/setOptions\", _clusterApi.SetClusterOptions);\n            _webService.MapPost(\"/api/admin/cluster/initJoin\", _clusterApi.InitializeAndJoinClusterAsync);\n            _webService.MapGetAndPost(\"/api/admin/cluster/secondary/leave\", _clusterApi.LeaveClusterAsync);\n            _webService.MapGetAndPost(\"/api/admin/cluster/secondary/notify\", _clusterApi.ConfigUpdateNotificationAsync);\n            _webService.MapGetAndPost(\"/api/admin/cluster/secondary/resync\", _clusterApi.ResyncCluster);\n            _webService.MapGetAndPost(\"/api/admin/cluster/secondary/updatePrimary\", _clusterApi.UpdatePrimaryNodeAsync);\n            _webService.MapGetAndPost(\"/api/admin/cluster/secondary/promote\", _clusterApi.PromoteToPrimaryNodeAsync);\n            _webService.MapGetAndPost(\"/api/admin/cluster/updateIpAddress\", _clusterApi.UpdateSelfNodeIPAddress);\n\n            //logs\n            _webService.MapGetAndPost(\"/api/logs/list\", _logsApi.ListLogs);\n            _webService.MapGetAndPost(\"/api/logs/download\", _logsApi.DownloadLogAsync);\n            _webService.MapGetAndPost(\"/api/logs/delete\", _logsApi.DeleteLog);\n            _webService.MapGetAndPost(\"/api/logs/deleteAll\", _logsApi.DeleteAllLogs);\n            _webService.MapGetAndPost(\"/api/logs/query\", _logsApi.QueryLogsAsync);\n            _webService.MapGetAndPost(\"/api/logs/export\", _logsApi.ExportLogsAsync);\n\n            //fallback\n            _webService.MapFallback(\"/api/{*path}\", delegate (HttpContext context)\n            {\n                //mark api fallback\n                context.Items[\"apiFallback\"] = string.Empty;\n            });\n        }\n\n        private static ClusterNodeType GetClusterNodeTypeForPath(string path)\n        {\n            switch (path)\n            {\n                case \"/api/user/createToken\":\n                case \"/api/user/changePassword\":\n                case \"/api/user/2fa/init\":\n                case \"/api/user/2fa/enable\":\n                case \"/api/user/2fa/disable\":\n                case \"/api/user/profile/set\":\n\n                case \"/api/allowed/add\":\n                case \"/api/allowed/delete\":\n                case \"/api/allowed/flush\":\n                case \"/api/allowed/import\":\n\n                case \"/api/blocked/add\":\n                case \"/api/blocked/delete\":\n                case \"/api/blocked/flush\":\n                case \"/api/blocked/import\":\n\n                case \"/api/apps/downloadAndInstall\":\n                case \"/api/apps/downloadAndUpdate\":\n                case \"/api/apps/install\":\n                case \"/api/apps/update\":\n                case \"/api/apps/uninstall\":\n                case \"/api/apps/config/set\":\n\n                case \"/api/admin/sessions/createToken\":\n                case \"/api/admin/users/create\":\n                case \"/api/admin/users/set\":\n                case \"/api/admin/users/delete\":\n                case \"/api/admin/groups/create\":\n                case \"/api/admin/groups/set\":\n                case \"/api/admin/groups/delete\":\n                    return ClusterNodeType.Primary; //this api can be called only on primary node\n\n                case \"/api/user/login\":\n                case \"/api/user/logout\":\n                case \"/api/user/session/get\":\n                case \"/api/user/session/delete\":\n                    return ClusterNodeType.Secondary; //this api must be called on current node\n\n                default:\n                    return ClusterNodeType.Unknown; //this api can be called on any specified node\n            }\n        }\n\n        private Task WebServiceHttpsRedirectionMiddleware(HttpContext context, RequestDelegate next)\n        {\n            if (context.Request.IsHttps)\n                return next(context);\n\n            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);\n            return Task.CompletedTask;\n        }\n\n        private async Task WebServiceApiMiddleware(HttpContext context, RequestDelegate next)\n        {\n            HttpRequest request = context.Request;\n\n            if (_clusterManager.ClusterInitialized)\n            {\n                ClusterNodeType pathNodeType = GetClusterNodeTypeForPath(request.Path);\n                switch (pathNodeType)\n                {\n                    case ClusterNodeType.Primary:\n                        //this api can be called only on primary node\n                        ClusterNode selfNode = _clusterManager.GetSelfNode();\n                        if (selfNode.Type == ClusterNodeType.Secondary)\n                        {\n                            //validate user session before proxying request\n                            if (!TryGetSession(context, out UserSession session))\n                                throw new InvalidTokenWebServiceException(\"Invalid token or session expired.\");\n\n                            //proxy to primary node\n                            ClusterNode primaryNode = _clusterManager.GetPrimaryNode();\n                            await primaryNode.ProxyRequest(context, session.User.Username);\n                            return;\n                        }\n\n                        break;\n\n                    case ClusterNodeType.Secondary:\n                        //this api must be called on current node\n                        break;\n\n                    default:\n                        //this api can be called on any specified node\n                        string nodeName = request.GetQueryOrForm(\"node\", null);\n                        if (!string.IsNullOrEmpty(nodeName) && (nodeName != \"cluster\"))\n                        {\n                            if (!_clusterManager.TryGetClusterNode(nodeName, out ClusterNode node))\n                                throw new DnsWebServiceException(\"No such node exists in the Cluster by name: \" + nodeName);\n\n                            if (node.State != ClusterNodeState.Self)\n                            {\n                                //validate user session before proxying request\n                                if (!TryGetSession(context, out UserSession session))\n                                    throw new InvalidTokenWebServiceException(\"Invalid token or session expired.\");\n\n                                //proxy request to the specified cluster node\n                                await node.ProxyRequest(context, session.User.Username);\n                                return;\n                            }\n                        }\n\n                        break;\n                }\n            }\n\n            bool needsJsonResponseObject;\n\n            switch (request.Path)\n            {\n                case \"/api/user/login\":\n                case \"/api/user/createToken\":\n                case \"/api/user/logout\":\n                    needsJsonResponseObject = false;\n                    break;\n\n                case \"/api/user/session/get\":\n                    {\n                        if (!TryGetSession(context, out UserSession session))\n                            throw new InvalidTokenWebServiceException(\"Invalid token or session expired.\");\n\n                        context.Items[\"session\"] = session;\n\n                        needsJsonResponseObject = false;\n                    }\n                    break;\n\n                case \"/api/zones/export\":\n                case \"/api/allowed/export\":\n                case \"/api/blocked/export\":\n                case \"/api/settings/backup\":\n                case \"/api/logs/download\":\n                case \"/api/logs/export\":\n                case \"/api/admin/cluster/primary/transferConfig\":\n                    {\n                        if (!TryGetSession(context, out UserSession session))\n                            throw new InvalidTokenWebServiceException(\"Invalid token or session expired.\");\n\n                        context.Items[\"session\"] = session;\n\n                        await next(context);\n                    }\n                    return;\n\n                default:\n                    if (request.Path.Value.StartsWith(\"/api/\", StringComparison.OrdinalIgnoreCase))\n                    {\n                        if (!TryGetSession(context, out UserSession session))\n                            throw new InvalidTokenWebServiceException(\"Invalid token or session expired.\");\n\n                        context.Items[\"session\"] = session;\n                        needsJsonResponseObject = true;\n                    }\n                    else\n                    {\n                        HttpResponse response = context.Response;\n                        response.StatusCode = StatusCodes.Status404NotFound;\n                        response.ContentLength = 0;\n                        response.Headers.CacheControl = \"no-cache, no-store, must-revalidate\";\n                        response.Headers.Pragma = \"no-cache\";\n                        response.Headers.Expires = \"0\";\n                        return;\n                    }\n\n                    break;\n            }\n\n            using (MemoryStream mS = new MemoryStream(4096))\n            {\n                Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS);\n                context.Items[\"jsonWriter\"] = jsonWriter;\n\n                jsonWriter.WriteStartObject();\n\n                if (needsJsonResponseObject)\n                {\n                    jsonWriter.WritePropertyName(\"response\");\n                    jsonWriter.WriteStartObject();\n\n                    await next(context);\n\n                    jsonWriter.WriteEndObject();\n                }\n                else\n                {\n                    await next(context);\n                }\n\n                jsonWriter.WriteString(\"server\", _dnsServer.ServerDomain);\n                jsonWriter.WriteString(\"status\", \"ok\");\n\n                jsonWriter.WriteEndObject();\n                jsonWriter.Flush();\n\n                mS.Position = 0;\n\n                HttpResponse response = context.Response;\n\n                response.Headers.CacheControl = \"no-cache, no-store, must-revalidate\";\n                response.Headers.Pragma = \"no-cache\";\n                response.Headers.Expires = \"0\";\n\n                object apiFallback = context.Items[\"apiFallback\"]; //check api fallback mark\n                if (apiFallback is null)\n                {\n                    response.StatusCode = StatusCodes.Status200OK;\n                    response.ContentType = \"application/json; charset=utf-8\";\n                    response.ContentLength = mS.Length;\n\n                    await mS.CopyToAsync(response.Body);\n                }\n                else\n                {\n                    response.StatusCode = StatusCodes.Status404NotFound;\n                    response.ContentLength = 0;\n                }\n            }\n        }\n\n        private void WebServiceExceptionHandler(IApplicationBuilder exceptionHandlerApp)\n        {\n            exceptionHandlerApp.Run(async delegate (HttpContext context)\n            {\n                IExceptionHandlerPathFeature exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();\n                if (exceptionHandlerPathFeature.Path.StartsWith(\"/api/\"))\n                {\n                    Exception ex = exceptionHandlerPathFeature.Error;\n\n                    HttpResponse response = context.Response;\n\n                    response.StatusCode = StatusCodes.Status200OK;\n                    response.Headers.CacheControl = \"no-cache, no-store, must-revalidate\";\n                    response.Headers.Pragma = \"no-cache\";\n                    response.Headers.Expires = \"0\";\n                    response.ContentType = \"application/json; charset=utf-8\";\n\n                    await using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(response.Body))\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"server\", _dnsServer.ServerDomain);\n\n                        if (ex is TwoFactorAuthRequiredWebServiceException)\n                        {\n                            jsonWriter.WriteString(\"status\", \"2fa-required\");\n                            jsonWriter.WriteString(\"errorMessage\", ex.Message);\n                        }\n                        else if (ex is InvalidTokenWebServiceException)\n                        {\n                            jsonWriter.WriteString(\"status\", \"invalid-token\");\n                            jsonWriter.WriteString(\"errorMessage\", ex.Message);\n                        }\n                        else\n                        {\n                            _log.Write(context.GetRemoteEndPoint(_webServiceRealIpHeader), ex);\n\n                            jsonWriter.WriteString(\"status\", \"error\");\n                            jsonWriter.WriteString(\"errorMessage\", ex.Message);\n                            jsonWriter.WriteString(\"stackTrace\", ex.StackTrace);\n\n                            if (ex.InnerException is not null)\n                                jsonWriter.WriteString(\"innerErrorMessage\", ex.InnerException.Message);\n                        }\n\n                        jsonWriter.WriteEndObject();\n                    }\n                }\n            });\n        }\n\n        private bool TryGetSession(HttpContext context, out UserSession session)\n        {\n            string token = context.Request.GetQueryOrForm(\"token\");\n            session = _authManager.GetSession(token);\n            if ((session is null) || session.User.Disabled)\n                return false;\n\n            if (session.HasExpired())\n            {\n                _authManager.DeleteSession(session.Token);\n                _authManager.SaveConfigFile();\n                return false;\n            }\n\n            IPEndPoint remoteEP = context.GetRemoteEndPoint(_webServiceRealIpHeader);\n\n            session.UpdateLastSeen(remoteEP.Address, context.Request.Headers.UserAgent);\n            return true;\n        }\n\n        private User GetSessionUser(HttpContext context, bool standardOnly = false)\n        {\n            UserSession session = context.GetCurrentSession();\n\n            if ((session.Type == UserSessionType.ApiToken) && _clusterManager.ClusterInitialized && session.TokenName.Equals(_clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))\n            {\n                //proxy call from cluster node \n                string username = context.Request.GetQueryOrForm(\"actingUser\", null);\n                if (username is null)\n                    return session.User;\n\n                User user = _authManager.GetUser(username);\n                if (user is null)\n                    throw new DnsWebServiceException(\"No such user exists: \" + username);\n\n                return user;\n            }\n            else\n            {\n                if (standardOnly && (session.Type != UserSessionType.Standard))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                return session.User;\n            }\n        }\n\n        #endregion\n\n        #region tls\n\n        private void StartTlsCertificateUpdateTimer()\n        {\n            if (_tlsCertificateUpdateTimer is null)\n            {\n                _tlsCertificateUpdateTimer = new Timer(delegate (object state)\n                {\n                    if (!string.IsNullOrEmpty(_webServiceTlsCertificatePath))\n                    {\n                        string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);\n\n                        try\n                        {\n                            FileInfo fileInfo = new FileInfo(webServiceTlsCertificatePath);\n\n                            if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _webServiceCertificateLastModifiedOn))\n                            {\n                                LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, _webServiceTlsCertificatePassword);\n\n                                if (_clusterManager.ClusterInitialized)\n                                    _clusterManager.UpdateSelfNodeUrlAndCertificate();\n                            }\n                        }\n                        catch (Exception ex)\n                        {\n                            _log.Write(\"DNS Server encountered an error while updating Web Service TLS Certificate: \" + webServiceTlsCertificatePath + \"\\r\\n\" + ex.ToString());\n                        }\n                    }\n                }, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL);\n            }\n        }\n\n        private void StopTlsCertificateUpdateTimer()\n        {\n            if (_tlsCertificateUpdateTimer is not null)\n            {\n                _tlsCertificateUpdateTimer.Dispose();\n                _tlsCertificateUpdateTimer = null;\n            }\n        }\n\n        private void LoadWebServiceTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword)\n        {\n            FileInfo fileInfo = new FileInfo(tlsCertificatePath);\n\n            if (!fileInfo.Exists)\n                throw new ArgumentException(\"Web Service TLS certificate file does not exists: \" + tlsCertificatePath);\n\n            switch (Path.GetExtension(tlsCertificatePath).ToLowerInvariant())\n            {\n                case \".pfx\":\n                case \".p12\":\n                    break;\n\n                default:\n                    throw new ArgumentException(\"Web Service TLS certificate file must be PKCS #12 formatted with .pfx or .p12 extension: \" + tlsCertificatePath);\n            }\n\n            X509Certificate2Collection certificateCollection = X509CertificateLoader.LoadPkcs12CollectionFromFile(tlsCertificatePath, tlsCertificatePassword, X509KeyStorageFlags.PersistKeySet);\n            X509Certificate2 serverCertificate = null;\n\n            foreach (X509Certificate2 certificate in certificateCollection)\n            {\n                if (certificate.HasPrivateKey)\n                {\n                    serverCertificate = certificate;\n                    break;\n                }\n            }\n\n            if (serverCertificate is null)\n                throw new ArgumentException(\"Web Service TLS certificate file must contain a certificate with private key.\");\n\n            List<SslApplicationProtocol> applicationProtocols = new List<SslApplicationProtocol>();\n\n            if (_webServiceEnableHttp3)\n                applicationProtocols.Add(new SslApplicationProtocol(\"h3\"));\n\n            if (IsHttp2Supported())\n                applicationProtocols.Add(new SslApplicationProtocol(\"h2\"));\n\n            applicationProtocols.Add(new SslApplicationProtocol(\"http/1.1\"));\n\n            _webServiceSslServerAuthenticationOptions = new SslServerAuthenticationOptions\n            {\n                ApplicationProtocols = applicationProtocols,\n                ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, certificateCollection, false)\n            };\n\n            _webServiceCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc;\n\n            _log.Write(\"Web Service TLS certificate was loaded: \" + tlsCertificatePath);\n        }\n\n        private void RemoveWebServiceTlsCertificate()\n        {\n            _webServiceSslServerAuthenticationOptions = null;\n\n            _webServiceTlsCertificatePath = null;\n            _webServiceTlsCertificatePassword = null;\n\n            StopTlsCertificateUpdateTimer();\n        }\n\n        public void SetWebServiceTlsCertificate(string webServiceTlsCertificatePath, string webServiceTlsCertificatePassword)\n        {\n            if (string.IsNullOrWhiteSpace(webServiceTlsCertificatePath))\n                throw new ArgumentException(\"Web service TLS certificate path cannot be null or empty.\", nameof(webServiceTlsCertificatePath));\n\n            if (webServiceTlsCertificatePath.Length > 255)\n                throw new ArgumentException(\"Web service TLS certificate path length cannot exceed 255 characters.\", nameof(webServiceTlsCertificatePath));\n\n            if (webServiceTlsCertificatePassword?.Length > 255)\n                throw new ArgumentException(\"Web service TLS certificate password length cannot exceed 255 characters.\", nameof(webServiceTlsCertificatePassword));\n\n            webServiceTlsCertificatePath = ConvertToAbsolutePath(webServiceTlsCertificatePath);\n\n            try\n            {\n                LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, webServiceTlsCertificatePassword);\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DNS Server encountered an error while loading Web Service TLS Certificate: \" + webServiceTlsCertificatePath + \"\\r\\n\" + ex.ToString());\n            }\n\n            _webServiceTlsCertificatePath = ConvertToRelativePath(webServiceTlsCertificatePath);\n            _webServiceTlsCertificatePassword = webServiceTlsCertificatePassword;\n\n            StartTlsCertificateUpdateTimer();\n        }\n\n        private void CheckAndLoadSelfSignedCertificate(bool forceGenerateNew, bool throwException)\n        {\n            string selfSignedCertificateFilePath = Path.Combine(_configFolder, \"self-signed-cert.pfx\");\n\n            if (_webServiceUseSelfSignedTlsCertificate)\n            {\n                string oldSelfSignedCertificateFilePath = Path.Combine(_configFolder, \"cert.pfx\");\n\n                if (!oldSelfSignedCertificateFilePath.Equals(ConvertToAbsolutePath(_webServiceTlsCertificatePath), Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) && File.Exists(oldSelfSignedCertificateFilePath) && !File.Exists(selfSignedCertificateFilePath))\n                    File.Move(oldSelfSignedCertificateFilePath, selfSignedCertificateFilePath);\n\n                if (forceGenerateNew || !File.Exists(selfSignedCertificateFilePath))\n                {\n                    RSA rsa = RSA.Create(2048);\n                    CertificateRequest req = new CertificateRequest(\"cn=\" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n\n                    SubjectAlternativeNameBuilder san = new SubjectAlternativeNameBuilder();\n                    bool sanAdded = false;\n\n                    foreach (IPAddress localAddress in _webServiceLocalAddresses)\n                    {\n                        if (localAddress.Equals(IPAddress.IPv6Any) || localAddress.Equals(IPAddress.Any))\n                            continue;\n\n                        san.AddIpAddress(localAddress);\n                        sanAdded = true;\n                    }\n\n                    if (sanAdded)\n                        req.CertificateExtensions.Add(san.Build());\n\n                    X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5));\n\n                    File.WriteAllBytes(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string));\n                }\n\n                if (_webServiceEnableTls && string.IsNullOrEmpty(_webServiceTlsCertificatePath))\n                {\n                    try\n                    {\n                        LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null);\n\n                        if (!forceGenerateNew)\n                        {\n                            if (_webServiceSslServerAuthenticationOptions.ServerCertificateContext.TargetCertificate.NotAfter < DateTime.UtcNow.AddYears(1))\n                            {\n                                _log.Write(\"Web Service TLS self signed certificate is nearing expiration and will be regenerated.\");\n                                CheckAndLoadSelfSignedCertificate(true, throwException); //force generate new cert\n\n                                if (_clusterManager.ClusterInitialized)\n                                    _clusterManager.UpdateSelfNodeUrlAndCertificate();\n                            }\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(\"DNS Server encountered an error while loading self signed Web Service TLS certificate: \" + selfSignedCertificateFilePath + \"\\r\\n\" + ex.ToString());\n\n                        if (throwException)\n                            throw;\n                    }\n                }\n            }\n            else\n            {\n                File.Delete(selfSignedCertificateFilePath);\n            }\n        }\n\n        #endregion\n\n        #region quic\n\n        private static void ValidateQuicSupport(string protocolName = \"DNS-over-QUIC\")\n        {\n#pragma warning disable CA2252 // This API requires opting into preview features\n#pragma warning disable CA1416 // Validate platform compatibility\n\n            if (!QuicConnection.IsSupported)\n                throw new DnsWebServiceException(protocolName + \" is supported only on Windows 11, Windows Server 2022, and Linux. On Linux, you must install 'libmsquic' manually.\");\n\n#pragma warning restore CA1416 // Validate platform compatibility\n#pragma warning restore CA2252 // This API requires opting into preview features\n        }\n\n        private static bool IsQuicSupported()\n        {\n#pragma warning disable CA2252 // This API requires opting into preview features\n#pragma warning disable CA1416 // Validate platform compatibility\n\n            return QuicConnection.IsSupported;\n\n#pragma warning restore CA1416 // Validate platform compatibility\n#pragma warning restore CA2252 // This API requires opting into preview features\n        }\n\n        #endregion\n\n        #region secondary catalog zones\n\n        private void AuthZoneManager_SecondaryCatalogZoneAdded(object sender, SecondaryCatalogEventArgs e)\n        {\n            AuthZoneInfo secondaryCatalogZoneInfo = new AuthZoneInfo(sender as ApexZone);\n            AuthZoneInfo memberZoneInfo = e.ZoneInfo;\n\n            //clone user/group permissions from source zone\n            Permission sourceZonePermissions = _authManager.GetPermission(PermissionSection.Zones, secondaryCatalogZoneInfo.Name);\n\n            foreach (KeyValuePair<User, PermissionFlag> userPermission in sourceZonePermissions.UserPermissions)\n                _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, userPermission.Key, userPermission.Value);\n\n            foreach (KeyValuePair<Group, PermissionFlag> groupPermissions in sourceZonePermissions.GroupPermissions)\n                _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, groupPermissions.Key, groupPermissions.Value);\n\n            //set default permissions\n            _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n            _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n            _authManager.SaveConfigFile();\n\n            //sync dnssec private keys for secondary members zone when it is a cluster secondary catalog zone\n            if (_clusterManager.ClusterInitialized && (memberZoneInfo.Type == AuthZoneType.Secondary) && secondaryCatalogZoneInfo.Name.Equals(\"cluster-catalog.\" + _clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))\n                _clusterManager.TriggerRefreshForConfig([memberZoneInfo.Name]);\n\n            //delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zone\n            _dnsServer.CacheZoneManager.DeleteZone(memberZoneInfo.Name);\n        }\n\n        private void AuthZoneManager_SecondaryCatalogZoneRemoved(object sender, SecondaryCatalogEventArgs e)\n        {\n            _authManager.RemoveAllPermissions(PermissionSection.Zones, e.ZoneInfo.Name);\n            _authManager.SaveConfigFile();\n\n            //delete cache for this zone to allow rebuilding cache data without using the current zone\n            _dnsServer.CacheZoneManager.DeleteZone(e.ZoneInfo.Name);\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task StartAsync(bool throwIfBindFails = false)\n        {\n            if (_disposed)\n                ObjectDisposedException.ThrowIf(_disposed, this);\n\n            if (_isRunning)\n                throw new DnsWebServiceException(\"The DNS web service is already running.\");\n\n            try\n            {\n                //init dns server\n                _dnsServer = new DnsServer(_configFolder, Path.Combine(_appFolder, \"dohwww\"), _log);\n\n                //init dhcp server\n                _dhcpServer = new DhcpServer(Path.Combine(_configFolder, \"scopes\"), _log);\n                _dhcpServer.DnsServer = _dnsServer;\n                _dhcpServer.AuthManager = _authManager;\n\n                //load web service config file\n                LoadConfigFile();\n\n                //load dns config file\n                _dnsServer.LoadConfigFile();\n\n                //load all dns applications\n                await _dnsServer.DnsApplicationManager.LoadAllApplicationsAsync();\n\n                //load all zones files\n                _dnsServer.AuthZoneManager.SecondaryCatalogZoneAdded += AuthZoneManager_SecondaryCatalogZoneAdded;\n                _dnsServer.AuthZoneManager.SecondaryCatalogZoneRemoved += AuthZoneManager_SecondaryCatalogZoneRemoved;\n                _dnsServer.AuthZoneManager.LoadAllZoneFiles();\n                InspectAndFixZonePermissions();\n\n                //disable zones from old config format\n                if (_configDisabledZones != null)\n                {\n                    foreach (string domain in _configDisabledZones)\n                    {\n                        AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(domain);\n                        if (zoneInfo is not null)\n                        {\n                            zoneInfo.Disabled = true;\n                            _dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);\n                        }\n                    }\n                }\n\n                //load allowed zone and blocked zone files\n                _dnsServer.AllowedZoneManager.LoadAllowedZoneFile();\n                _dnsServer.BlockedZoneManager.LoadBlockedZoneFile();\n                _dnsServer.BlockListZoneManager.LoadConfigFile();\n\n                //init cluster manager\n                _clusterManager = new ClusterManager(this);\n\n                //load cluster config file\n                _clusterManager.LoadConfigFile();\n\n                //start web service\n                if (throwIfBindFails)\n                    await StartWebServiceAsync(false);\n                else\n                    await TryStartWebServiceAsync([IPAddress.Any, IPAddress.IPv6Any], 5380, 53443);\n\n                //start dns and dhcp\n                await _dnsServer.StartAsync(throwIfBindFails);\n                _dhcpServer.Start();\n\n                _log.Write(\"DNS Server (v\" + _currentVersion.ToString() + \") was started successfully.\");\n                _isRunning = true;\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"Failed to start DNS Server (v\" + _currentVersion.ToString() + \")\\r\\n\" + ex.ToString());\n                throw;\n            }\n        }\n\n        public async Task StopAsync()\n        {\n            if (!_isRunning || _disposed)\n                return;\n\n            try\n            {\n                //stop cluster manager\n                _clusterManager?.Dispose();\n\n                //stop web service\n                await StopWebServiceAsync();\n\n                //stop dhcp\n                _dhcpServer?.Dispose();\n\n                //stop dns & save cache to disk\n                if (_dnsServer is not null)\n                    await _dnsServer.DisposeAsync();\n\n                _log.Write(\"DNS Server (v\" + _currentVersion.ToString() + \") was stopped successfully.\");\n                _isRunning = false;\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"Failed to stop DNS Server (v\" + _currentVersion.ToString() + \")\\r\\n\" + ex.ToString());\n                throw;\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public DnsServer DnsServer\n        { get { return _dnsServer; } }\n\n        public DateTime UpTimeStamp\n        { get { return _uptimestamp; } }\n\n        public string ConfigFolder\n        { get { return _configFolder; } }\n\n        public int WebServiceHttpPort\n        { get { return _webServiceHttpPort; } }\n\n        public int WebServiceTlsPort\n        { get { return _webServiceTlsPort; } }\n\n        internal bool IsWebServiceTlsEnabled\n        {\n            get\n            {\n                return _webServiceEnableTls && (_webServiceUseSelfSignedTlsCertificate || !string.IsNullOrEmpty(_webServiceTlsCertificatePath)) && (_webServiceSslServerAuthenticationOptions is not null);\n            }\n        }\n\n        internal X509Certificate2 WebServiceTlsCertificate\n        {\n            get\n            {\n                if (_webServiceSslServerAuthenticationOptions is null)\n                    return null;\n\n                return _webServiceSslServerAuthenticationOptions.ServerCertificateContext.TargetCertificate;\n            }\n        }\n\n        internal AuthManager AuthManager\n        { get { return _authManager; } }\n\n        internal LogManager LogManager\n        { get { return _log; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/DnsWebServiceException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore\n{\n    public class DnsWebServiceException : Exception\n    {\n        #region constructors\n\n        public DnsWebServiceException()\n            : base()\n        { }\n\n        public DnsWebServiceException(string message)\n            : base(message)\n        { }\n\n        public DnsWebServiceException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/DnsWebServiceLegacy.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Dns;\nusing DnsServerCore.Dns.ZoneManagers;\nusing DnsServerCore.Dns.Zones;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Mail;\nusing System.Net.Sockets;\nusing System.Text;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ClientConnection;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        #region legacy config\n\n        private bool TryLoadOldConfigFile()\n        {\n            string configFile = Path.Combine(_configFolder, \"dns.config\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read))\n                {\n                    if (TryLoadOldConfigFrom(fS))\n                    {\n                        _log.Write(\"Old DNS config file was loaded: \" + configFile);\n                        return true;\n                    }\n                }\n            }\n            catch (FileNotFoundException)\n            {\n                //do nothing\n            }\n            catch (Exception ex)\n            {\n                _log.Write(\"DNS Server encountered an error while trying to load old DNS config file: \" + configFile + \"\\r\\n\" + ex.ToString());\n            }\n\n            return false;\n        }\n\n        private bool TryLoadOldConfigFrom(Stream s)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) == \"DS\")\n            {\n                int version = bR.ReadByte();\n\n                ReadOldConfigFrom(bR, version);\n\n                s.Dispose();\n                _dnsServer.SaveConfigFileInternal();\n\n                return true;\n            }\n\n            return false;\n        }\n\n        private void ReadOldConfigFrom(BinaryReader bR, int version)\n        {\n            if ((version >= 28) && (version <= 42))\n            {\n                ReadConfigFromV42(bR, version);\n            }\n            else if ((version >= 2) && (version <= 27))\n            {\n                ReadConfigFromV27(bR, version);\n\n                //new default settings\n                DnsClientConnection.IPv4SourceAddresses = null;\n                DnsClientConnection.IPv6SourceAddresses = null;\n                _dnsServer.EnableUdpSocketPool = Environment.OSVersion.Platform == PlatformID.Win32NT;\n                UdpClientConnection.SocketPoolExcludedPorts = [(ushort)_webServiceTlsPort];\n                _dnsServer.MaxConcurrentResolutionsPerCore = 100;\n                _dnsServer.DnsApplicationManager.EnableAutomaticUpdate = true;\n                _webServiceEnableHttp3 = _webServiceEnableTls && IsQuicSupported();\n                _dnsServer.EnableDnsOverHttp3 = _dnsServer.EnableDnsOverHttps && IsQuicSupported();\n                _webServiceRealIpHeader = \"X-Real-IP\";\n                _dnsServer.DnsOverHttpRealIpHeader = \"X-Real-IP\";\n                _dnsServer.DefaultResponsiblePerson = null;\n                _dnsServer.AuthZoneManager.UseSoaSerialDateScheme = false;\n                _dnsServer.AuthZoneManager.MinSoaRefresh = 300;\n                _dnsServer.AuthZoneManager.MinSoaRetry = 300;\n                _dnsServer.ZoneTransferAllowedNetworks = null;\n                _dnsServer.NotifyAllowedNetworks = null;\n                _dnsServer.EDnsClientSubnet = false;\n                _dnsServer.EDnsClientSubnetIPv4PrefixLength = 24;\n                _dnsServer.EDnsClientSubnetIPv6PrefixLength = 56;\n                _dnsServer.EDnsClientSubnetIpv4Override = null;\n                _dnsServer.EDnsClientSubnetIpv6Override = null;\n                _dnsServer.QpmLimitBypassList = null;\n\n                if (_dnsServer.EnableDnsOverUdpProxy || _dnsServer.EnableDnsOverTcpProxy || _dnsServer.EnableDnsOverHttp)\n                {\n                    _dnsServer.ReverseProxyNetworkACL =\n                        [\n                            new NetworkAccessControl(IPAddress.Parse(\"127.0.0.0\"), 8),\n                            new NetworkAccessControl(IPAddress.Parse(\"10.0.0.0\"), 8),\n                            new NetworkAccessControl(IPAddress.Parse(\"100.64.0.0\"), 10),\n                            new NetworkAccessControl(IPAddress.Parse(\"169.254.0.0\"), 16),\n                            new NetworkAccessControl(IPAddress.Parse(\"172.16.0.0\"), 12),\n                            new NetworkAccessControl(IPAddress.Parse(\"192.168.0.0\"), 16),\n                            new NetworkAccessControl(IPAddress.Parse(\"2000::\"), 3, true),\n                            new NetworkAccessControl(IPAddress.IPv6Any, 0)\n                        ];\n                }\n\n                _dnsServer.BlockingBypassList = null;\n                _dnsServer.BlockingAnswerTtl = 30;\n                _dnsServer.ResolverConcurrency = 2;\n                _dnsServer.CacheZoneManager.ServeStaleAnswerTtl = CacheZoneManager.SERVE_STALE_ANSWER_TTL;\n                _dnsServer.CacheZoneManager.ServeStaleResetTtl = CacheZoneManager.SERVE_STALE_RESET_TTL;\n                _dnsServer.ServeStaleMaxWaitTime = DnsServer.SERVE_STALE_MAX_WAIT_TIME;\n                _dnsServer.ConcurrentForwarding = true;\n                _dnsServer.ResolverLogManager = _log;\n                _dnsServer.StatsManager.EnableInMemoryStats = false;\n            }\n            else\n            {\n                throw new InvalidDataException(\"DNS Server config version not supported.\");\n            }\n        }\n\n        private void ReadConfigFromV42(BinaryReader bR, int version)\n        {\n            //web service\n            {\n                _webServiceHttpPort = bR.ReadInt32();\n                _webServiceTlsPort = bR.ReadInt32();\n\n                {\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        IPAddress[] localAddresses = new IPAddress[count];\n\n                        for (int i = 0; i < count; i++)\n                            localAddresses[i] = IPAddressExtensions.ReadFrom(bR);\n\n                        _webServiceLocalAddresses = localAddresses;\n                    }\n                    else\n                    {\n                        _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any };\n                    }\n                }\n\n                _webServiceEnableTls = bR.ReadBoolean();\n\n                if (version >= 33)\n                    _webServiceEnableHttp3 = bR.ReadBoolean();\n                else\n                    _webServiceEnableHttp3 = _webServiceEnableTls && IsQuicSupported();\n\n                _webServiceHttpToTlsRedirect = bR.ReadBoolean();\n                _webServiceUseSelfSignedTlsCertificate = bR.ReadBoolean();\n\n                _webServiceTlsCertificatePath = bR.ReadShortString();\n                _webServiceTlsCertificatePassword = bR.ReadShortString();\n\n                if (_webServiceTlsCertificatePath.Length == 0)\n                    _webServiceTlsCertificatePath = null;\n\n                if (_webServiceTlsCertificatePath is null)\n                {\n                    StopTlsCertificateUpdateTimer();\n                }\n                else\n                {\n                    string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);\n\n                    try\n                    {\n                        LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, _webServiceTlsCertificatePassword);\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(\"DNS Server encountered an error while loading Web Service TLS certificate: \" + webServiceTlsCertificatePath + \"\\r\\n\" + ex.ToString());\n                    }\n\n                    StartTlsCertificateUpdateTimer();\n                }\n\n                CheckAndLoadSelfSignedCertificate(false, false);\n\n                if (version >= 38)\n                    _webServiceRealIpHeader = bR.ReadShortString();\n                else\n                    _webServiceRealIpHeader = \"X-Real-IP\";\n            }\n\n            //dns\n            {\n                //general\n                _dnsServer.ServerDomain = bR.ReadShortString();\n\n                {\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        List<IPEndPoint> localEndPoints = new List<IPEndPoint>(count);\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            IPEndPoint ep = EndPointExtensions.ReadFrom(bR) as IPEndPoint;\n                            if (ep.Port == 853)\n                                continue; //to avoid validation exception\n\n                            localEndPoints.Add(ep);\n                        }\n\n                        _dnsServer.LocalEndPoints = localEndPoints;\n                    }\n                    else\n                    {\n                        _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) };\n                    }\n                }\n\n                if (version >= 34)\n                {\n                    DnsClientConnection.IPv4SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                    DnsClientConnection.IPv6SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                }\n                else\n                {\n                    DnsClientConnection.IPv4SourceAddresses = null;\n                    DnsClientConnection.IPv6SourceAddresses = null;\n                }\n\n                _dnsServer.AuthZoneManager.DefaultRecordTtl = bR.ReadUInt32();\n\n                if (version >= 36)\n                {\n                    string rp = bR.ReadString();\n                    if (rp.Length == 0)\n                        _dnsServer.DefaultResponsiblePerson = null;\n                    else\n                        _dnsServer.DefaultResponsiblePerson = new MailAddress(rp);\n                }\n                else\n                {\n                    _dnsServer.DefaultResponsiblePerson = null;\n                }\n\n                if (version >= 33)\n                    _dnsServer.AuthZoneManager.UseSoaSerialDateScheme = bR.ReadBoolean();\n                else\n                    _dnsServer.AuthZoneManager.UseSoaSerialDateScheme = false;\n\n                if (version >= 40)\n                {\n                    _dnsServer.AuthZoneManager.MinSoaRefresh = bR.ReadUInt32();\n                    _dnsServer.AuthZoneManager.MinSoaRetry = bR.ReadUInt32();\n                }\n                else\n                {\n                    _dnsServer.AuthZoneManager.MinSoaRefresh = 300;\n                    _dnsServer.AuthZoneManager.MinSoaRetry = 300;\n                }\n\n                if (version >= 33)\n                    _dnsServer.ZoneTransferAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                else\n                    _dnsServer.ZoneTransferAllowedNetworks = null;\n\n                if (version >= 34)\n                    _dnsServer.NotifyAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                else\n                    _dnsServer.NotifyAllowedNetworks = null;\n\n                _dnsServer.DnsApplicationManager.EnableAutomaticUpdate = bR.ReadBoolean();\n\n                _dnsServer.PreferIPv6 = bR.ReadBoolean();\n\n                if (version >= 42)\n                {\n                    _dnsServer.EnableUdpSocketPool = bR.ReadBoolean();\n\n                    int count = bR.ReadUInt16();\n                    ushort[] socketPoolExcludedPorts = new ushort[count];\n\n                    for (int i = 0; i < count; i++)\n                        socketPoolExcludedPorts[i] = bR.ReadUInt16();\n\n                    UdpClientConnection.SocketPoolExcludedPorts = socketPoolExcludedPorts;\n                }\n                else\n                {\n                    _dnsServer.EnableUdpSocketPool = Environment.OSVersion.Platform == PlatformID.Win32NT;\n                    UdpClientConnection.SocketPoolExcludedPorts = [(ushort)_webServiceTlsPort];\n                }\n\n                _dnsServer.UdpPayloadSize = bR.ReadUInt16();\n                _dnsServer.DnssecValidation = bR.ReadBoolean();\n\n                if (version >= 29)\n                {\n                    _dnsServer.EDnsClientSubnet = bR.ReadBoolean();\n                    _dnsServer.EDnsClientSubnetIPv4PrefixLength = bR.ReadByte();\n                    _dnsServer.EDnsClientSubnetIPv6PrefixLength = bR.ReadByte();\n                }\n                else\n                {\n                    _dnsServer.EDnsClientSubnet = false;\n                    _dnsServer.EDnsClientSubnetIPv4PrefixLength = 24;\n                    _dnsServer.EDnsClientSubnetIPv6PrefixLength = 56;\n                }\n\n                if (version >= 35)\n                {\n                    if (bR.ReadBoolean())\n                        _dnsServer.EDnsClientSubnetIpv4Override = NetworkAddress.ReadFrom(bR);\n                    else\n                        _dnsServer.EDnsClientSubnetIpv4Override = null;\n\n                    if (bR.ReadBoolean())\n                        _dnsServer.EDnsClientSubnetIpv6Override = NetworkAddress.ReadFrom(bR);\n                    else\n                        _dnsServer.EDnsClientSubnetIpv6Override = null;\n                }\n                else\n                {\n                    _dnsServer.EDnsClientSubnetIpv4Override = null;\n                    _dnsServer.EDnsClientSubnetIpv6Override = null;\n                }\n\n                if (version >= 42)\n                {\n                    {\n                        int count = bR.ReadByte();\n                        Dictionary<int, (int, int)> qpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>(count);\n\n                        for (int i = 0; i < count; i++)\n                            qpmPrefixLimitsIPv4.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32()));\n\n                        _dnsServer.QpmPrefixLimitsIPv4 = qpmPrefixLimitsIPv4;\n                    }\n\n                    {\n                        int count = bR.ReadByte();\n                        Dictionary<int, (int, int)> qpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>(count);\n\n                        for (int i = 0; i < count; i++)\n                            qpmPrefixLimitsIPv6.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32()));\n\n                        _dnsServer.QpmPrefixLimitsIPv6 = qpmPrefixLimitsIPv6;\n                    }\n\n                    _dnsServer.QpmLimitSampleMinutes = bR.ReadInt32();\n                    _dnsServer.QpmLimitUdpTruncationPercentage = bR.ReadInt32();\n                }\n                else\n                {\n                    int qpmLimitRequests = bR.ReadInt32();\n                    _ = bR.ReadInt32(); //obsolete qpmLimitErrors\n                    int qpmLimitSampleMinutes = bR.ReadInt32();\n                    int qpmLimitIPv4PrefixLength = bR.ReadInt32();\n                    int qpmLimitIPv6PrefixLength = bR.ReadInt32();\n\n                    _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>()\n                    {\n                        { qpmLimitIPv4PrefixLength, (qpmLimitRequests, qpmLimitRequests) }\n                    };\n\n                    _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>()\n                    {\n                        { qpmLimitIPv6PrefixLength, (qpmLimitRequests, qpmLimitRequests) }\n                    };\n\n                    _dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes;\n                    _dnsServer.QpmLimitUdpTruncationPercentage = 0;\n                }\n\n                if (version >= 34)\n                    _dnsServer.QpmLimitBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                else\n                    _dnsServer.QpmLimitBypassList = null;\n\n                _dnsServer.ClientTimeout = bR.ReadInt32();\n                if (version < 34)\n                {\n                    if (_dnsServer.ClientTimeout == 4000)\n                        _dnsServer.ClientTimeout = 2000;\n                }\n\n                _dnsServer.TcpSendTimeout = bR.ReadInt32();\n                _dnsServer.TcpReceiveTimeout = bR.ReadInt32();\n\n                if (version >= 30)\n                {\n                    _dnsServer.QuicIdleTimeout = bR.ReadInt32();\n                    _dnsServer.QuicMaxInboundStreams = bR.ReadInt32();\n                    _dnsServer.ListenBacklog = bR.ReadInt32();\n                }\n                else\n                {\n                    _dnsServer.QuicIdleTimeout = 60000;\n                    _dnsServer.QuicMaxInboundStreams = 100;\n                    _dnsServer.ListenBacklog = 100;\n                }\n\n                if (version >= 40)\n                    _dnsServer.MaxConcurrentResolutionsPerCore = bR.ReadUInt16();\n                else\n                    _dnsServer.MaxConcurrentResolutionsPerCore = 100;\n\n                //optional protocols\n                if (version >= 32)\n                {\n                    _dnsServer.EnableDnsOverUdpProxy = bR.ReadBoolean();\n                    _dnsServer.EnableDnsOverTcpProxy = bR.ReadBoolean();\n                }\n                else\n                {\n                    _dnsServer.EnableDnsOverUdpProxy = false;\n                    _dnsServer.EnableDnsOverTcpProxy = false;\n                }\n\n                _dnsServer.EnableDnsOverHttp = bR.ReadBoolean();\n                _dnsServer.EnableDnsOverTls = bR.ReadBoolean();\n                _dnsServer.EnableDnsOverHttps = bR.ReadBoolean();\n\n                if (version >= 37)\n                    _dnsServer.EnableDnsOverHttp3 = bR.ReadBoolean();\n                else\n                    _dnsServer.EnableDnsOverHttp3 = _dnsServer.EnableDnsOverHttps && IsQuicSupported();\n\n                if (version >= 32)\n                {\n                    _dnsServer.EnableDnsOverQuic = bR.ReadBoolean();\n\n                    _dnsServer.DnsOverUdpProxyPort = bR.ReadInt32();\n                    _dnsServer.DnsOverTcpProxyPort = bR.ReadInt32();\n                    _dnsServer.DnsOverHttpPort = bR.ReadInt32();\n                    _dnsServer.DnsOverTlsPort = bR.ReadInt32();\n                    _dnsServer.DnsOverHttpsPort = bR.ReadInt32();\n                    _dnsServer.DnsOverQuicPort = bR.ReadInt32();\n                }\n                else if (version >= 31)\n                {\n                    _dnsServer.EnableDnsOverQuic = bR.ReadBoolean();\n\n                    _dnsServer.DnsOverHttpPort = bR.ReadInt32();\n                    _dnsServer.DnsOverTlsPort = bR.ReadInt32();\n                    _dnsServer.DnsOverHttpsPort = bR.ReadInt32();\n                    _dnsServer.DnsOverQuicPort = bR.ReadInt32();\n                }\n                else if (version >= 30)\n                {\n                    _ = bR.ReadBoolean(); //removed EnableDnsOverHttpPort80 value\n                    _dnsServer.EnableDnsOverQuic = bR.ReadBoolean();\n\n                    _dnsServer.DnsOverHttpPort = bR.ReadInt32();\n                    _dnsServer.DnsOverTlsPort = bR.ReadInt32();\n                    _dnsServer.DnsOverHttpsPort = bR.ReadInt32();\n                    _dnsServer.DnsOverQuicPort = bR.ReadInt32();\n                }\n                else\n                {\n                    _dnsServer.EnableDnsOverQuic = false;\n\n                    _dnsServer.DnsOverUdpProxyPort = 538;\n                    _dnsServer.DnsOverTcpProxyPort = 538;\n\n                    if (_dnsServer.EnableDnsOverHttps)\n                    {\n                        _dnsServer.EnableDnsOverHttp = true;\n                        _dnsServer.DnsOverHttpPort = 80;\n                    }\n                    else if (_dnsServer.EnableDnsOverHttp)\n                    {\n                        _dnsServer.DnsOverHttpPort = 8053;\n                    }\n                    else\n                    {\n                        _dnsServer.DnsOverHttpPort = 80;\n                    }\n\n                    _dnsServer.DnsOverTlsPort = 853;\n                    _dnsServer.DnsOverHttpsPort = 443;\n                    _dnsServer.DnsOverQuicPort = 853;\n                }\n\n                if (version >= 39)\n                {\n                    _dnsServer.ReverseProxyNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR);\n                }\n                else\n                {\n                    if (_dnsServer.EnableDnsOverUdpProxy || _dnsServer.EnableDnsOverTcpProxy || _dnsServer.EnableDnsOverHttp)\n                    {\n                        _dnsServer.ReverseProxyNetworkACL =\n                            [\n                                new NetworkAccessControl(IPAddress.Parse(\"127.0.0.0\"), 8),\n                                new NetworkAccessControl(IPAddress.Parse(\"10.0.0.0\"), 8),\n                                new NetworkAccessControl(IPAddress.Parse(\"100.64.0.0\"), 10),\n                                new NetworkAccessControl(IPAddress.Parse(\"169.254.0.0\"), 16),\n                                new NetworkAccessControl(IPAddress.Parse(\"172.16.0.0\"), 12),\n                                new NetworkAccessControl(IPAddress.Parse(\"192.168.0.0\"), 16),\n                                new NetworkAccessControl(IPAddress.Parse(\"2000::\"), 3, true),\n                                new NetworkAccessControl(IPAddress.IPv6Any, 0)\n                            ];\n                    }\n                }\n\n                string dnsTlsCertificatePath = bR.ReadShortString();\n                string dnsTlsCertificatePassword = bR.ReadShortString();\n\n                if (dnsTlsCertificatePath.Length == 0)\n                    dnsTlsCertificatePath = null;\n\n                if (dnsTlsCertificatePath is null)\n                    _dnsServer.RemoveDnsTlsCertificate();\n                else\n                    _dnsServer.SetDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword);\n\n                if (version >= 38)\n                    _dnsServer.DnsOverHttpRealIpHeader = bR.ReadShortString();\n                else\n                    _dnsServer.DnsOverHttpRealIpHeader = \"X-Real-IP\";\n\n                //tsig\n                {\n                    int count = bR.ReadByte();\n                    Dictionary<string, TsigKey> tsigKeys = new Dictionary<string, TsigKey>(count);\n\n                    for (int i = 0; i < count; i++)\n                    {\n                        string keyName = bR.ReadShortString();\n                        string sharedSecret = bR.ReadShortString();\n                        TsigAlgorithm algorithm = (TsigAlgorithm)bR.ReadByte();\n\n                        tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithm));\n                    }\n\n                    _dnsServer.TsigKeys = tsigKeys;\n                }\n\n                //recursion\n                _dnsServer.Recursion = (DnsServerRecursion)bR.ReadByte();\n\n                if (version >= 37)\n                {\n                    _dnsServer.RecursionNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR);\n                }\n                else\n                {\n                    NetworkAddress[] recursionDeniedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                    NetworkAddress[] recursionAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                    _dnsServer.RecursionNetworkACL = AuthZoneInfo.ConvertDenyAllowToACL(recursionDeniedNetworks, recursionAllowedNetworks);\n                }\n\n                _dnsServer.RandomizeName = bR.ReadBoolean();\n                _dnsServer.QnameMinimization = bR.ReadBoolean();\n\n                if (version <= 40)\n                    _ = bR.ReadBoolean(); //removed NsRevalidation option\n\n                _dnsServer.ResolverRetries = bR.ReadInt32();\n                _dnsServer.ResolverTimeout = bR.ReadInt32();\n\n                if (version >= 37)\n                    _dnsServer.ResolverConcurrency = bR.ReadInt32();\n                else\n                    _dnsServer.ResolverConcurrency = 2;\n\n                _dnsServer.ResolverMaxStackCount = bR.ReadInt32();\n\n                //cache\n                if (version >= 30)\n                    _dnsServer.SaveCacheToDisk = bR.ReadBoolean();\n                else\n                    _dnsServer.SaveCacheToDisk = true;\n\n                _dnsServer.ServeStale = bR.ReadBoolean();\n                _dnsServer.CacheZoneManager.ServeStaleTtl = bR.ReadUInt32();\n\n                if (version >= 36)\n                {\n                    _dnsServer.CacheZoneManager.ServeStaleAnswerTtl = bR.ReadUInt32();\n                    _dnsServer.CacheZoneManager.ServeStaleResetTtl = bR.ReadUInt32();\n                    _dnsServer.ServeStaleMaxWaitTime = bR.ReadInt32();\n                }\n                else\n                {\n                    _dnsServer.CacheZoneManager.ServeStaleAnswerTtl = CacheZoneManager.SERVE_STALE_ANSWER_TTL;\n                    _dnsServer.CacheZoneManager.ServeStaleResetTtl = CacheZoneManager.SERVE_STALE_RESET_TTL;\n                    _dnsServer.ServeStaleMaxWaitTime = DnsServer.SERVE_STALE_MAX_WAIT_TIME;\n                }\n\n                _dnsServer.CacheZoneManager.MaximumEntries = bR.ReadInt64();\n                _dnsServer.CacheZoneManager.MinimumRecordTtl = bR.ReadUInt32();\n                _dnsServer.CacheZoneManager.MaximumRecordTtl = bR.ReadUInt32();\n                _dnsServer.CacheZoneManager.NegativeRecordTtl = bR.ReadUInt32();\n                _dnsServer.CacheZoneManager.FailureRecordTtl = bR.ReadUInt32();\n\n                _dnsServer.CachePrefetchEligibility = bR.ReadInt32();\n                _dnsServer.CachePrefetchTrigger = bR.ReadInt32();\n                _dnsServer.CachePrefetchSampleIntervalMinutes = bR.ReadInt32();\n                _dnsServer.CachePrefetchSampleEligibilityHitsPerHour = bR.ReadInt32();\n\n                //blocking\n                _dnsServer.EnableBlocking = bR.ReadBoolean();\n                _dnsServer.AllowTxtBlockingReport = bR.ReadBoolean();\n\n                if (version >= 33)\n                    _dnsServer.BlockingBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR);\n                else\n                    _dnsServer.BlockingBypassList = null;\n\n                _dnsServer.BlockingType = (DnsServerBlockingType)bR.ReadByte();\n\n                if (version >= 38)\n                    _dnsServer.BlockingAnswerTtl = bR.ReadUInt32();\n                else\n                    _dnsServer.BlockingAnswerTtl = 30;\n\n                {\n                    //read custom blocking addresses\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        List<DnsARecordData> dnsARecords = new List<DnsARecordData>();\n                        List<DnsAAAARecordData> dnsAAAARecords = new List<DnsAAAARecordData>();\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            IPAddress customAddress = IPAddressExtensions.ReadFrom(bR);\n\n                            switch (customAddress.AddressFamily)\n                            {\n                                case AddressFamily.InterNetwork:\n                                    dnsARecords.Add(new DnsARecordData(customAddress));\n                                    break;\n\n                                case AddressFamily.InterNetworkV6:\n                                    dnsAAAARecords.Add(new DnsAAAARecordData(customAddress));\n                                    break;\n                            }\n                        }\n\n                        _dnsServer.CustomBlockingARecords = dnsARecords;\n                        _dnsServer.CustomBlockingAAAARecords = dnsAAAARecords;\n                    }\n                    else\n                    {\n                        _dnsServer.CustomBlockingARecords = null;\n                        _dnsServer.CustomBlockingAAAARecords = null;\n                    }\n                }\n\n                {\n                    //read block list urls\n                    int count = bR.ReadByte();\n                    string[] blockListUrls = new string[count];\n\n                    for (int i = 0; i < count; i++)\n                        blockListUrls[i] = bR.ReadShortString();\n\n                    _dnsServer.BlockListZoneManager.BlockListUrls = blockListUrls;\n\n                    _dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = bR.ReadInt32();\n                    _dnsServer.BlockListZoneManager.BlockListLastUpdatedOn = bR.ReadDateTime();\n                }\n\n                //proxy & forwarders\n                NetProxyType proxyType = (NetProxyType)bR.ReadByte();\n                if (proxyType != NetProxyType.None)\n                {\n                    string address = bR.ReadShortString();\n                    int port = bR.ReadInt32();\n                    NetworkCredential credential = null;\n\n                    if (bR.ReadBoolean()) //credential set\n                        credential = new NetworkCredential(bR.ReadShortString(), bR.ReadShortString());\n\n                    _dnsServer.Proxy = NetProxy.CreateProxy(proxyType, address, port, credential);\n\n                    int count = bR.ReadByte();\n                    List<NetProxyBypassItem> bypassList = new List<NetProxyBypassItem>(count);\n\n                    for (int i = 0; i < count; i++)\n                        bypassList.Add(new NetProxyBypassItem(bR.ReadShortString()));\n\n                    _dnsServer.Proxy.BypassList = bypassList;\n                }\n                else\n                {\n                    _dnsServer.Proxy = null;\n                }\n\n                {\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        NameServerAddress[] forwarders = new NameServerAddress[count];\n\n                        for (int i = 0; i < count; i++)\n                        {\n                            forwarders[i] = new NameServerAddress(bR);\n\n                            if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson)\n                                forwarders[i] = forwarders[i].Clone(DnsTransportProtocol.Https);\n                        }\n\n                        _dnsServer.Forwarders = forwarders;\n                    }\n                    else\n                    {\n                        _dnsServer.Forwarders = null;\n                    }\n                }\n\n                if (version >= 37)\n                    _dnsServer.ConcurrentForwarding = bR.ReadBoolean();\n                else\n                    _dnsServer.ConcurrentForwarding = true;\n\n                _dnsServer.ForwarderRetries = bR.ReadInt32();\n                _dnsServer.ForwarderTimeout = bR.ReadInt32();\n                _dnsServer.ForwarderConcurrency = bR.ReadInt32();\n\n                //logging\n                if (version >= 33)\n                {\n                    if (bR.ReadBoolean()) //ignore resolver logs\n                        _dnsServer.ResolverLogManager = null;\n                    else\n                        _dnsServer.ResolverLogManager = _log;\n                }\n                else\n                {\n                    _dnsServer.ResolverLogManager = _log;\n                }\n\n                if (bR.ReadBoolean()) //log all queries\n                    _dnsServer.QueryLogManager = _log;\n                else\n                    _dnsServer.QueryLogManager = null;\n\n                if (version >= 34)\n                    _dnsServer.StatsManager.EnableInMemoryStats = bR.ReadBoolean();\n                else\n                    _dnsServer.StatsManager.EnableInMemoryStats = false;\n\n                {\n                    int maxStatFileDays = bR.ReadInt32();\n                    if (maxStatFileDays < 0)\n                        maxStatFileDays = 0;\n\n                    _dnsServer.StatsManager.MaxStatFileDays = maxStatFileDays;\n                }\n            }\n        }\n\n        private void ReadConfigFromV27(BinaryReader bR, int version)\n        {\n            _dnsServer.ServerDomain = bR.ReadShortString();\n            _webServiceHttpPort = bR.ReadInt32();\n\n            if (version >= 13)\n            {\n                {\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        IPAddress[] localAddresses = new IPAddress[count];\n\n                        for (int i = 0; i < count; i++)\n                            localAddresses[i] = IPAddressExtensions.ReadFrom(bR);\n\n                        _webServiceLocalAddresses = localAddresses;\n                    }\n                    else\n                    {\n                        _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any };\n                    }\n                }\n\n                _webServiceTlsPort = bR.ReadInt32();\n                _webServiceEnableTls = bR.ReadBoolean();\n                _webServiceHttpToTlsRedirect = bR.ReadBoolean();\n                _webServiceTlsCertificatePath = bR.ReadShortString();\n                _webServiceTlsCertificatePassword = bR.ReadShortString();\n\n                if (_webServiceTlsCertificatePath.Length == 0)\n                    _webServiceTlsCertificatePath = null;\n\n                if (_webServiceTlsCertificatePath is null)\n                {\n                    StopTlsCertificateUpdateTimer();\n                }\n                else\n                {\n                    string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);\n\n                    try\n                    {\n                        LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, _webServiceTlsCertificatePassword);\n                    }\n                    catch (Exception ex)\n                    {\n                        _log.Write(\"DNS Server encountered an error while loading Web Service TLS certificate: \" + webServiceTlsCertificatePath + \"\\r\\n\" + ex.ToString());\n                    }\n\n                    StartTlsCertificateUpdateTimer();\n                }\n            }\n            else\n            {\n                _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any };\n\n                _webServiceTlsPort = 53443;\n                _webServiceEnableTls = false;\n                _webServiceHttpToTlsRedirect = false;\n                _webServiceTlsCertificatePath = string.Empty;\n                _webServiceTlsCertificatePassword = string.Empty;\n            }\n\n            _dnsServer.PreferIPv6 = bR.ReadBoolean();\n\n            if (bR.ReadBoolean()) //logQueries\n                _dnsServer.QueryLogManager = _log;\n\n            if (version >= 14)\n            {\n                int maxStatFileDays = bR.ReadInt32();\n                if (maxStatFileDays < 0)\n                    maxStatFileDays = 0;\n\n                _dnsServer.StatsManager.MaxStatFileDays = maxStatFileDays;\n            }\n            else\n            {\n                _dnsServer.StatsManager.MaxStatFileDays = 0;\n            }\n\n            if (version >= 17)\n            {\n                _dnsServer.Recursion = (DnsServerRecursion)bR.ReadByte();\n\n                NetworkAddress[] recursionDeniedNetworks;\n                {\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        NetworkAddress[] networks = new NetworkAddress[count];\n\n                        for (int i = 0; i < count; i++)\n                            networks[i] = NetworkAddress.ReadFrom(bR);\n\n                        recursionDeniedNetworks = networks;\n                    }\n                    else\n                    {\n                        recursionDeniedNetworks = null;\n                    }\n                }\n\n                NetworkAddress[] recursionAllowedNetworks;\n                {\n                    int count = bR.ReadByte();\n                    if (count > 0)\n                    {\n                        NetworkAddress[] networks = new NetworkAddress[count];\n\n                        for (int i = 0; i < count; i++)\n                            networks[i] = NetworkAddress.ReadFrom(bR);\n\n                        recursionAllowedNetworks = networks;\n                    }\n                    else\n                    {\n                        recursionAllowedNetworks = null;\n                    }\n                }\n\n                _dnsServer.RecursionNetworkACL = AuthZoneInfo.ConvertDenyAllowToACL(recursionDeniedNetworks, recursionAllowedNetworks);\n            }\n            else\n            {\n                bool allowRecursion = bR.ReadBoolean();\n                bool allowRecursionOnlyForPrivateNetworks;\n\n                if (version >= 4)\n                    allowRecursionOnlyForPrivateNetworks = bR.ReadBoolean();\n                else\n                    allowRecursionOnlyForPrivateNetworks = true; //default true for security reasons\n\n                if (allowRecursion)\n                {\n                    if (allowRecursionOnlyForPrivateNetworks)\n                        _dnsServer.Recursion = DnsServerRecursion.AllowOnlyForPrivateNetworks;\n                    else\n                        _dnsServer.Recursion = DnsServerRecursion.Allow;\n                }\n                else\n                {\n                    _dnsServer.Recursion = DnsServerRecursion.Deny;\n                }\n            }\n\n            if (version >= 12)\n                _dnsServer.RandomizeName = bR.ReadBoolean();\n            else\n                _dnsServer.RandomizeName = false; //default false to allow resolving from bad name servers\n\n            if (version >= 15)\n                _dnsServer.QnameMinimization = bR.ReadBoolean();\n            else\n                _dnsServer.QnameMinimization = true; //default true to enable privacy feature\n\n            if (version >= 20)\n            {\n                int qpmLimitRequests = bR.ReadInt32();\n                _ = bR.ReadInt32(); //obsolete qpmLimitErrors\n                int qpmLimitSampleMinutes = bR.ReadInt32();\n                int qpmLimitIPv4PrefixLength = bR.ReadInt32();\n                int qpmLimitIPv6PrefixLength = bR.ReadInt32();\n\n                _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>()\n                {\n                    { qpmLimitIPv4PrefixLength, (qpmLimitRequests, qpmLimitRequests) }\n                };\n\n                _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>()\n                {\n                    { qpmLimitIPv6PrefixLength, (qpmLimitRequests, qpmLimitRequests) }\n                };\n\n                _dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes;\n                _dnsServer.QpmLimitUdpTruncationPercentage = 0;\n            }\n            else if (version >= 17)\n            {\n                int qpmLimitRequests = bR.ReadInt32();\n                int qpmLimitSampleMinutes = bR.ReadInt32();\n                _ = bR.ReadInt32(); //read obsolete value _dnsServer.QpmLimitSamplingIntervalInMinutes\n\n                _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>()\n                {\n                    { 24, (qpmLimitRequests, qpmLimitRequests) }\n                };\n\n                _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>()\n                {\n                    { 56, (qpmLimitRequests, qpmLimitRequests) }\n                };\n\n                _dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes;\n                _dnsServer.QpmLimitUdpTruncationPercentage = 0;\n            }\n            else\n            {\n                _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary<int, (int, int)>()\n                {\n                    { 32, (600, 600) },\n                    { 24, (6000, 6000) }\n                };\n\n                _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary<int, (int, int)>()\n                {\n                    { 128, (600, 600) },\n                    { 64, (1200, 1200) },\n                    { 56, (6000, 6000) }\n                };\n\n                _dnsServer.QpmLimitSampleMinutes = 5;\n                _dnsServer.QpmLimitUdpTruncationPercentage = 50;\n            }\n\n            if (version >= 13)\n            {\n                _dnsServer.ServeStale = bR.ReadBoolean();\n                _dnsServer.CacheZoneManager.ServeStaleTtl = bR.ReadUInt32();\n            }\n            else\n            {\n                _dnsServer.ServeStale = true;\n                _dnsServer.CacheZoneManager.ServeStaleTtl = CacheZoneManager.SERVE_STALE_TTL;\n            }\n\n            if (version >= 9)\n            {\n                _dnsServer.CachePrefetchEligibility = bR.ReadInt32();\n                _dnsServer.CachePrefetchTrigger = bR.ReadInt32();\n                _dnsServer.CachePrefetchSampleIntervalMinutes = bR.ReadInt32();\n                _dnsServer.CachePrefetchSampleEligibilityHitsPerHour = bR.ReadInt32();\n            }\n            else\n            {\n                _dnsServer.CachePrefetchEligibility = 2;\n                _dnsServer.CachePrefetchTrigger = 9;\n                _dnsServer.CachePrefetchSampleIntervalMinutes = 5;\n                _dnsServer.CachePrefetchSampleEligibilityHitsPerHour = 30;\n            }\n\n            NetProxyType proxyType = (NetProxyType)bR.ReadByte();\n            if (proxyType != NetProxyType.None)\n            {\n                string address = bR.ReadShortString();\n                int port = bR.ReadInt32();\n                NetworkCredential credential = null;\n\n                if (bR.ReadBoolean()) //credential set\n                    credential = new NetworkCredential(bR.ReadShortString(), bR.ReadShortString());\n\n                _dnsServer.Proxy = NetProxy.CreateProxy(proxyType, address, port, credential);\n\n                if (version >= 10)\n                {\n                    int count = bR.ReadByte();\n                    List<NetProxyBypassItem> bypassList = new List<NetProxyBypassItem>(count);\n\n                    for (int i = 0; i < count; i++)\n                        bypassList.Add(new NetProxyBypassItem(bR.ReadShortString()));\n\n                    _dnsServer.Proxy.BypassList = bypassList;\n                }\n                else\n                {\n                    _dnsServer.Proxy.BypassList = null;\n                }\n            }\n            else\n            {\n                _dnsServer.Proxy = null;\n            }\n\n            {\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    NameServerAddress[] forwarders = new NameServerAddress[count];\n\n                    for (int i = 0; i < count; i++)\n                    {\n                        forwarders[i] = new NameServerAddress(bR);\n                        if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson)\n                            forwarders[i] = forwarders[i].Clone(DnsTransportProtocol.Https);\n                    }\n\n                    _dnsServer.Forwarders = forwarders;\n                }\n                else\n                {\n                    _dnsServer.Forwarders = null;\n                }\n            }\n\n            if (version <= 10)\n            {\n                DnsTransportProtocol forwarderProtocol = (DnsTransportProtocol)bR.ReadByte();\n                if (forwarderProtocol == DnsTransportProtocol.HttpsJson)\n                    forwarderProtocol = DnsTransportProtocol.Https;\n\n                if (_dnsServer.Forwarders != null)\n                {\n                    List<NameServerAddress> forwarders = new List<NameServerAddress>();\n\n                    foreach (NameServerAddress forwarder in _dnsServer.Forwarders)\n                    {\n                        if (forwarder.Protocol == forwarderProtocol)\n                            forwarders.Add(forwarder);\n                        else\n                            forwarders.Add(forwarder.Clone(forwarderProtocol));\n                    }\n\n                    _dnsServer.Forwarders = forwarders;\n                }\n            }\n\n            {\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    if (version > 2)\n                    {\n                        for (int i = 0; i < count; i++)\n                        {\n                            string username = bR.ReadShortString();\n                            string passwordHash = bR.ReadShortString();\n\n                            if (username.Equals(\"admin\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                _authManager.LoadOldConfig(passwordHash, true);\n                                break;\n                            }\n                        }\n                    }\n                    else\n                    {\n                        for (int i = 0; i < count; i++)\n                        {\n                            string username = bR.ReadShortString();\n                            string password = bR.ReadShortString();\n\n                            if (username.Equals(\"admin\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                _authManager.LoadOldConfig(password, false);\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n\n            if (version <= 6)\n            {\n                int count = bR.ReadInt32();\n                _configDisabledZones = new List<string>(count);\n\n                for (int i = 0; i < count; i++)\n                {\n                    string domain = bR.ReadShortString();\n                    _configDisabledZones.Add(domain);\n                }\n            }\n\n            if (version >= 18)\n                _dnsServer.EnableBlocking = bR.ReadBoolean();\n            else\n                _dnsServer.EnableBlocking = true;\n\n            if (version >= 18)\n                _dnsServer.BlockingType = (DnsServerBlockingType)bR.ReadByte();\n            else if (version >= 16)\n                _dnsServer.BlockingType = bR.ReadBoolean() ? DnsServerBlockingType.NxDomain : DnsServerBlockingType.AnyAddress;\n            else\n                _dnsServer.BlockingType = DnsServerBlockingType.AnyAddress;\n\n            if (version >= 18)\n            {\n                //read custom blocking addresses\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    List<DnsARecordData> dnsARecords = new List<DnsARecordData>();\n                    List<DnsAAAARecordData> dnsAAAARecords = new List<DnsAAAARecordData>();\n\n                    for (int i = 0; i < count; i++)\n                    {\n                        IPAddress customAddress = IPAddressExtensions.ReadFrom(bR);\n\n                        switch (customAddress.AddressFamily)\n                        {\n                            case AddressFamily.InterNetwork:\n                                dnsARecords.Add(new DnsARecordData(customAddress));\n                                break;\n\n                            case AddressFamily.InterNetworkV6:\n                                dnsAAAARecords.Add(new DnsAAAARecordData(customAddress));\n                                break;\n                        }\n                    }\n\n                    _dnsServer.CustomBlockingARecords = dnsARecords;\n                    _dnsServer.CustomBlockingAAAARecords = dnsAAAARecords;\n                }\n                else\n                {\n                    _dnsServer.CustomBlockingARecords = null;\n                    _dnsServer.CustomBlockingAAAARecords = null;\n                }\n            }\n            else\n            {\n                _dnsServer.CustomBlockingARecords = null;\n                _dnsServer.CustomBlockingAAAARecords = null;\n            }\n\n            if (version > 4)\n            {\n                //read block list urls\n                int count = bR.ReadByte();\n                string[] blockListUrls = new string[count];\n\n                for (int i = 0; i < count; i++)\n                    blockListUrls[i] = bR.ReadShortString();\n\n                _dnsServer.BlockListZoneManager.BlockListUrls = blockListUrls;\n\n                _dnsServer.BlockListZoneManager.BlockListLastUpdatedOn = bR.ReadDateTime();\n\n                if (version >= 13)\n                    _dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = bR.ReadInt32();\n            }\n            else\n            {\n                _dnsServer.BlockListZoneManager.BlockListUrls = null;\n                _dnsServer.BlockListZoneManager.BlockListLastUpdatedOn = DateTime.MinValue;\n                _dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = 24;\n            }\n\n            if (version >= 11)\n            {\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    List<IPEndPoint> localEndPoints = new List<IPEndPoint>(count);\n\n                    for (int i = 0; i < count; i++)\n                    {\n                        IPEndPoint ep = EndPointExtensions.ReadFrom(bR) as IPEndPoint;\n                        if (ep.Port == 853)\n                            continue; //to avoid validation exception\n\n                        localEndPoints.Add(ep);\n                    }\n\n                    _dnsServer.LocalEndPoints = localEndPoints;\n                }\n                else\n                {\n                    _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) };\n                }\n            }\n            else if (version >= 6)\n            {\n                int count = bR.ReadByte();\n                if (count > 0)\n                {\n                    List<IPEndPoint> localEndPoints = new List<IPEndPoint>(count);\n\n                    for (int i = 0; i < count; i++)\n                    {\n                        IPEndPoint ep = EndPointExtensions.ReadFrom(bR) as IPEndPoint;\n                        if (ep.Port == 853)\n                            continue; //to avoid validation exception\n\n                        localEndPoints.Add(ep);\n                    }\n\n                    _dnsServer.LocalEndPoints = localEndPoints;\n                }\n                else\n                {\n                    _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) };\n                }\n            }\n            else\n            {\n                _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) };\n            }\n\n            if (version >= 8)\n            {\n                _dnsServer.EnableDnsOverHttp = bR.ReadBoolean();\n                _dnsServer.EnableDnsOverTls = bR.ReadBoolean();\n                _dnsServer.EnableDnsOverHttps = bR.ReadBoolean();\n                string dnsTlsCertificatePath = bR.ReadShortString();\n                string dnsTlsCertificatePassword = bR.ReadShortString();\n\n                if (dnsTlsCertificatePath.Length == 0)\n                    dnsTlsCertificatePath = null;\n\n                if (dnsTlsCertificatePath is null)\n                    _dnsServer.RemoveDnsTlsCertificate();\n                else\n                    _dnsServer.SetDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword);\n            }\n            else\n            {\n                _dnsServer.EnableDnsOverHttp = false;\n                _dnsServer.EnableDnsOverTls = false;\n                _dnsServer.EnableDnsOverHttps = false;\n\n                _dnsServer.RemoveDnsTlsCertificate();\n            }\n\n            if (version >= 19)\n            {\n                _dnsServer.CacheZoneManager.MinimumRecordTtl = bR.ReadUInt32();\n                _dnsServer.CacheZoneManager.MaximumRecordTtl = bR.ReadUInt32();\n                _dnsServer.CacheZoneManager.NegativeRecordTtl = bR.ReadUInt32();\n                _dnsServer.CacheZoneManager.FailureRecordTtl = bR.ReadUInt32();\n            }\n            else\n            {\n                _dnsServer.CacheZoneManager.MinimumRecordTtl = CacheZoneManager.MINIMUM_RECORD_TTL;\n                _dnsServer.CacheZoneManager.MaximumRecordTtl = CacheZoneManager.MAXIMUM_RECORD_TTL;\n                _dnsServer.CacheZoneManager.NegativeRecordTtl = CacheZoneManager.NEGATIVE_RECORD_TTL;\n                _dnsServer.CacheZoneManager.FailureRecordTtl = CacheZoneManager.FAILURE_RECORD_TTL;\n            }\n\n            if (version >= 21)\n            {\n                int count = bR.ReadByte();\n                Dictionary<string, TsigKey> tsigKeys = new Dictionary<string, TsigKey>(count);\n\n                for (int i = 0; i < count; i++)\n                {\n                    string keyName = bR.ReadShortString();\n                    string sharedSecret = bR.ReadShortString();\n                    TsigAlgorithm algorithm = (TsigAlgorithm)bR.ReadByte();\n\n                    tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithm));\n                }\n\n                _dnsServer.TsigKeys = tsigKeys;\n            }\n            else if (version >= 20)\n            {\n                int count = bR.ReadByte();\n                Dictionary<string, TsigKey> tsigKeys = new Dictionary<string, TsigKey>(count);\n\n                for (int i = 0; i < count; i++)\n                {\n                    string keyName = bR.ReadShortString();\n                    string sharedSecret = bR.ReadShortString();\n\n                    tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, TsigAlgorithm.HMAC_SHA256));\n                }\n\n                _dnsServer.TsigKeys = tsigKeys;\n            }\n            else\n            {\n                _dnsServer.TsigKeys = null;\n            }\n\n            if (version >= 22)\n                _ = bR.ReadBoolean(); //removed NsRevalidation option\n\n            if (version >= 23)\n            {\n                _dnsServer.AllowTxtBlockingReport = bR.ReadBoolean();\n                _dnsServer.AuthZoneManager.DefaultRecordTtl = bR.ReadUInt32();\n            }\n            else\n            {\n                _dnsServer.AllowTxtBlockingReport = true;\n                _dnsServer.AuthZoneManager.DefaultRecordTtl = 3600;\n            }\n\n            if (version >= 24)\n            {\n                _webServiceUseSelfSignedTlsCertificate = bR.ReadBoolean();\n\n                CheckAndLoadSelfSignedCertificate(false, false);\n            }\n            else\n            {\n                _webServiceUseSelfSignedTlsCertificate = false;\n            }\n\n            if (version >= 25)\n                _dnsServer.UdpPayloadSize = bR.ReadUInt16();\n            else\n                _dnsServer.UdpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE;\n\n            if (version >= 26)\n            {\n                _dnsServer.DnssecValidation = bR.ReadBoolean();\n\n                _dnsServer.ResolverRetries = bR.ReadInt32();\n                _dnsServer.ResolverTimeout = bR.ReadInt32();\n                _dnsServer.ResolverMaxStackCount = bR.ReadInt32();\n\n                _dnsServer.ForwarderRetries = bR.ReadInt32();\n                _dnsServer.ForwarderTimeout = bR.ReadInt32();\n                _dnsServer.ForwarderConcurrency = bR.ReadInt32();\n\n                _dnsServer.ClientTimeout = bR.ReadInt32();\n                if (_dnsServer.ClientTimeout == 4000)\n                    _dnsServer.ClientTimeout = 2000;\n\n                _dnsServer.TcpSendTimeout = bR.ReadInt32();\n                _dnsServer.TcpReceiveTimeout = bR.ReadInt32();\n            }\n            else\n            {\n                _dnsServer.DnssecValidation = true;\n                CreateForwarderZoneToDisableDnssecForNTP();\n\n                _dnsServer.ResolverRetries = 2;\n                _dnsServer.ResolverTimeout = 1500;\n                _dnsServer.ResolverMaxStackCount = 16;\n\n                _dnsServer.ForwarderRetries = 3;\n                _dnsServer.ForwarderTimeout = 2000;\n                _dnsServer.ForwarderConcurrency = 2;\n\n                _dnsServer.ClientTimeout = 2000;\n                _dnsServer.TcpSendTimeout = 10000;\n                _dnsServer.TcpReceiveTimeout = 10000;\n            }\n\n            if (version >= 27)\n                _dnsServer.CacheZoneManager.MaximumEntries = bR.ReadInt32();\n            else\n                _dnsServer.CacheZoneManager.MaximumEntries = 10000;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/Extensions.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing System;\nusing System.Net;\nusing System.Text.Json;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerCore\n{\n    static class Extensions\n    {\n        static readonly string[] HTTP_METHODS = new string[] { \"GET\", \"POST\" };\n        static readonly char[] COMMA_SEPARATOR = new char[] { ',' };\n\n        public static IPEndPoint GetRemoteEndPoint(this HttpContext context, string realIpHeaderName = null)\n        {\n            try\n            {\n                IPAddress remoteIP = context.Connection.RemoteIpAddress;\n                if (remoteIP is null)\n                    return new IPEndPoint(IPAddress.Any, 0);\n\n                if (remoteIP.IsIPv4MappedToIPv6)\n                    remoteIP = remoteIP.MapToIPv4();\n\n                if (!string.IsNullOrEmpty(realIpHeaderName) && NetUtilities.IsPrivateIP(remoteIP))\n                {\n                    //get the real IP address of the requesting client from X-Real-IP header set in nginx proxy_pass block\n                    string xRealIp = context.Request.Headers[realIpHeaderName];\n                    if (IPAddress.TryParse(xRealIp, out IPAddress address))\n                        return new IPEndPoint(address, 0);\n                }\n\n                return new IPEndPoint(remoteIP, context.Connection.RemotePort);\n            }\n            catch\n            {\n                return new IPEndPoint(IPAddress.Any, 0);\n            }\n        }\n\n        public static IPAddress GetLocalIpAddress(this HttpContext context)\n        {\n            try\n            {\n                IPAddress localIP = context.Connection.LocalIpAddress;\n                if (localIP is null)\n                    return IPAddress.Any;\n\n                if (localIP.IsIPv4MappedToIPv6)\n                    localIP = localIP.MapToIPv4();\n\n                return localIP;\n            }\n            catch\n            {\n                return IPAddress.Any;\n            }\n        }\n\n        public static UserSession GetCurrentSession(this HttpContext context)\n        {\n            if (context.Items[\"session\"] is UserSession userSession)\n                return userSession;\n\n            throw new InvalidOperationException();\n        }\n\n        public static Utf8JsonWriter GetCurrentJsonWriter(this HttpContext context)\n        {\n            if (context.Items[\"jsonWriter\"] is Utf8JsonWriter jsonWriter)\n                return jsonWriter;\n\n            throw new InvalidOperationException();\n        }\n\n        public static IEndpointConventionBuilder MapGetAndPost(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)\n        {\n            return endpoints.MapMethods(pattern, HTTP_METHODS, requestDelegate);\n        }\n\n        public static IEndpointConventionBuilder MapGetAndPost(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler)\n        {\n            return endpoints.MapMethods(pattern, HTTP_METHODS, handler);\n        }\n\n        public static string QueryOrForm(this HttpRequest request, string parameter)\n        {\n            if (request.HttpContext.Items.TryGetValue(\"jsonContent\", out object jsonObject))\n            {\n                JsonDocument json = (JsonDocument)jsonObject;\n\n                if (!json.RootElement.TryGetProperty(parameter, out JsonElement jsonValue))\n                    return null;\n\n                switch (jsonValue.ValueKind)\n                {\n                    case JsonValueKind.String:\n                        return jsonValue.GetString();\n\n                    case JsonValueKind.Number:\n                    case JsonValueKind.True:\n                    case JsonValueKind.False:\n                        return jsonValue.ToString();\n\n                    case JsonValueKind.Null:\n                        return null;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n            }\n\n            string value = request.Query[parameter];\n            if ((value is null) && request.HasFormContentType)\n                value = request.Form[parameter];\n\n            return value;\n        }\n\n        public static string GetQueryOrForm(this HttpRequest request, string parameter)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n                throw new DnsWebServiceException(\"Parameter '\" + parameter + \"' missing.\");\n\n            return value;\n        }\n\n        public static string GetQueryOrForm(this HttpRequest request, string parameter, string defaultValue)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n                return defaultValue;\n\n            return value;\n        }\n\n        public static T GetQueryOrForm<T>(this HttpRequest request, string parameter, Func<string, T> parse)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n                throw new DnsWebServiceException(\"Parameter '\" + parameter + \"' missing.\");\n\n            return parse(value);\n        }\n\n        public static T GetQueryOrFormEnum<T>(this HttpRequest request, string parameter) where T : struct\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n                throw new DnsWebServiceException(\"Parameter '\" + parameter + \"' missing.\");\n\n            return Enum.Parse<T>(value, true);\n        }\n\n        public static T GetQueryOrForm<T>(this HttpRequest request, string parameter, Func<string, T> parse, T defaultValue)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n                return defaultValue;\n\n            return parse(value);\n        }\n\n        public static T GetQueryOrFormEnum<T>(this HttpRequest request, string parameter, T defaultValue) where T : struct\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n                return defaultValue;\n\n            return Enum.Parse<T>(value, true);\n        }\n\n        public static bool TryGetQueryOrForm(this HttpRequest request, string parameter, out string value)\n        {\n            value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n                return false;\n\n            return true;\n        }\n\n        public static bool TryGetQueryOrForm<T>(this HttpRequest request, string parameter, Func<string, T> parse, out T value)\n        {\n            string strValue = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(strValue))\n            {\n                value = default;\n                return false;\n            }\n\n            value = parse(strValue);\n            return true;\n        }\n\n        public static bool TryGetQueryOrFormEnum<T>(this HttpRequest request, string parameter, out T value) where T : struct\n        {\n            string strValue = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(strValue))\n            {\n                value = default;\n                return false;\n            }\n\n            return Enum.TryParse(strValue, true, out value);\n        }\n\n        public static string GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n            {\n                value = request.QueryOrForm(alternateParameter);\n                if (string.IsNullOrEmpty(value))\n                    throw new DnsWebServiceException(\"Parameter '\" + parameter + \"' missing.\");\n            }\n\n            return value;\n        }\n\n        public static string GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter, string defaultValue)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n            {\n                value = request.QueryOrForm(alternateParameter);\n                if (string.IsNullOrEmpty(value))\n                    return defaultValue;\n            }\n\n            return value;\n        }\n\n        public static T GetQueryOrFormAlt<T>(this HttpRequest request, string parameter, string alternateParameter, Func<string, T> parse)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n            {\n                value = request.QueryOrForm(alternateParameter);\n                if (string.IsNullOrEmpty(value))\n                    throw new DnsWebServiceException(\"Parameter '\" + parameter + \"' missing.\");\n            }\n\n            return parse(value);\n        }\n\n        public static T GetQueryOrFormAlt<T>(this HttpRequest request, string parameter, string alternateParameter, Func<string, T> parse, T defaultValue)\n        {\n            string value = request.QueryOrForm(parameter);\n            if (string.IsNullOrEmpty(value))\n            {\n                value = request.QueryOrForm(alternateParameter);\n                if (string.IsNullOrEmpty(value))\n                    return defaultValue;\n            }\n\n            return parse(value);\n        }\n\n        public static bool TryGetQueryOrFormArray(this HttpRequest request, string parameter, out string[] array, params char[] separator)\n        {\n            if (request.HttpContext.Items.TryGetValue(\"jsonContent\", out object jsonObject))\n            {\n                JsonDocument json = (JsonDocument)jsonObject;\n\n                if (!json.RootElement.TryReadArray(parameter, out array))\n                    return false;\n\n                if (array is null)\n                    array = [];\n\n                return true;\n            }\n\n            string value = request.QueryOrForm(parameter);\n            if (value is null)\n            {\n                array = null;\n                return false;\n            }\n\n            if ((value.Length == 0) || value.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n            {\n                array = [];\n                return true;\n            }\n\n            if ((separator is null) || (separator.Length == 0))\n                separator = COMMA_SEPARATOR;\n\n            array = value.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n            return true;\n        }\n\n        public static bool TryGetQueryOrFormArray<T>(this HttpRequest request, string parameter, Func<string, T> parse, out T[] array, params char[] separator)\n        {\n            if (request.HttpContext.Items.TryGetValue(\"jsonContent\", out object jsonObject))\n            {\n                JsonDocument json = (JsonDocument)jsonObject;\n\n                if (!json.RootElement.TryReadArray(parameter, parse, out array))\n                    return false;\n\n                if (array is null)\n                    array = [];\n\n                return true;\n            }\n\n            string value = request.QueryOrForm(parameter);\n            if (value is null)\n            {\n                array = null;\n                return false;\n            }\n\n            if ((value.Length == 0) || value.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n            {\n                array = [];\n                return true;\n            }\n\n            if ((separator is null) || (separator.Length == 0))\n                separator = COMMA_SEPARATOR;\n\n            array = value.Split(parse, separator);\n            return true;\n        }\n\n        public static bool TryGetQueryOrFormArray<T>(this HttpRequest request, string parameter, Func<JsonElement, T> getObject, Func<ArraySegment<string>, T> parse, int colspan, out T[] array, params char[] separator)\n        {\n            if (request.HttpContext.Items.TryGetValue(\"jsonContent\", out object jsonObject))\n            {\n                JsonDocument json = (JsonDocument)jsonObject;\n\n                if (!json.RootElement.TryReadArray(parameter, getObject, out array))\n                    return false;\n\n                if (array is null)\n                    array = [];\n\n                return true;\n            }\n\n            string value = request.QueryOrForm(parameter);\n            if (value is null)\n            {\n                array = null;\n                return false;\n            }\n\n            if ((value.Length == 0) || value.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n            {\n                array = [];\n                return true;\n            }\n\n            if ((separator is null) || (separator.Length == 0))\n                separator = COMMA_SEPARATOR;\n\n            string[] cells = value.Split(separator);\n            array = new T[cells.Length / colspan];\n\n            for (int i = 0, j = 0; i < cells.Length; i += colspan)\n                array[j++] = parse(new ArraySegment<string>(cells, i, colspan));\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/InvalidTokenWebServiceException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore\n{\n    public class InvalidTokenWebServiceException : DnsWebServiceException\n    {\n        #region constructors\n\n        public InvalidTokenWebServiceException()\n            : base()\n        { }\n\n        public InvalidTokenWebServiceException(string message)\n            : base(message)\n        { }\n\n        public InvalidTokenWebServiceException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/LogManager.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Globalization;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Linq;\nusing System.Net;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Channels;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.EDnsOptions;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore\n{\n    [Flags]\n    public enum LoggingType : byte\n    {\n        None = 0,\n        File = 1,\n        Console = 2,\n        FileAndConsole = 3\n    }\n\n    public sealed class LogManager : IDisposable\n    {\n        #region variables\n\n        static readonly char[] commaSeparator = new char[] { ',' };\n\n        readonly string _configFolder;\n\n        LoggingType _loggingType;\n        string _logFolder;\n        int _maxLogFileDays;\n        bool _useLocalTime;\n\n        const string LOG_ENTRY_DATE_TIME_FORMAT = \"yyyy-MM-dd HH:mm:ss\";\n        const string LOG_FILE_DATE_TIME_FORMAT = \"yyyy-MM-dd\";\n\n        bool _isRunning;\n        string _logFile;\n        StreamWriter _logWriter;\n        DateTime _logDate;\n        readonly object _logFileLock = new object();\n\n        Channel<LogQueueItem> _channel;\n        ChannelWriter<LogQueueItem> _channelWriter;\n        Thread _consumerThread;\n\n        readonly Timer _logCleanupTimer;\n        const int LOG_CLEANUP_TIMER_INITIAL_INTERVAL = 60 * 1000;\n        const int LOG_CLEANUP_TIMER_PERIODIC_INTERVAL = 60 * 60 * 1000;\n\n        readonly object _saveLock = new object();\n        bool _pendingSave;\n        readonly Timer _saveTimer;\n        const int SAVE_TIMER_INITIAL_INTERVAL = 5000;\n\n        #endregion\n\n        #region constructor\n\n        public LogManager(string configFolder)\n        {\n            _configFolder = configFolder;\n\n            AppDomain.CurrentDomain.UnhandledException += delegate (object sender, UnhandledExceptionEventArgs e)\n            {\n                string logEntry = GetLogEntry(DateTime.UtcNow, e.ExceptionObject.ToString());\n\n                Console.WriteLine(logEntry);\n\n                if (_loggingType.HasFlag(LoggingType.File))\n                {\n                    lock (_logFileLock)\n                    {\n                        WriteLogEntry(logEntry);\n                    }\n                }\n            };\n\n            _logCleanupTimer = new Timer(delegate (object state)\n            {\n                try\n                {\n                    if (_maxLogFileDays < 1)\n                        return;\n\n                    DateTime cutoffDate = DateTime.UtcNow.AddDays(_maxLogFileDays * -1).Date;\n                    DateTimeStyles dateTimeStyles;\n\n                    if (_useLocalTime)\n                        dateTimeStyles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;\n                    else\n                        dateTimeStyles = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal;\n\n                    foreach (string logFile in ListLogFiles())\n                    {\n                        string logFileName = Path.GetFileNameWithoutExtension(logFile);\n\n                        if (!DateTime.TryParseExact(logFileName, LOG_FILE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, dateTimeStyles, out DateTime logFileDate))\n                            continue;\n\n                        if (logFileDate < cutoffDate)\n                        {\n                            try\n                            {\n                                File.Delete(logFile);\n                                Write(\"LogManager cleanup deleted the log file: \" + logFile);\n                            }\n                            catch (Exception ex)\n                            {\n                                Write(ex);\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    Write(ex);\n                }\n            });\n\n            LoadConfigFile();\n\n            _saveTimer = new Timer(delegate (object state)\n            {\n                lock (_saveLock)\n                {\n                    if (_pendingSave)\n                    {\n                        try\n                        {\n                            SaveConfigFileInternal();\n                            _pendingSave = false;\n                        }\n                        catch (Exception ex)\n                        {\n                            Write(ex);\n\n                            //set timer to retry again\n                            _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n                        }\n                    }\n                }\n            });\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _logCleanupTimer?.Dispose();\n\n            StopLogging();\n\n            lock (_saveLock)\n            {\n                _saveTimer?.Dispose();\n\n                if (_pendingSave)\n                {\n                    try\n                    {\n                        SaveConfigFileInternal();\n                    }\n                    catch (Exception ex)\n                    {\n                        Write(ex);\n                    }\n                    finally\n                    {\n                        _pendingSave = false;\n                    }\n                }\n            }\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region config\n\n        private void LoadConfigFile()\n        {\n            string logConfigFile = Path.Combine(_configFolder, \"log.config\");\n\n            try\n            {\n                using (FileStream fS = new FileStream(logConfigFile, FileMode.Open, FileAccess.Read))\n                {\n                    ReadConfigFrom(fS);\n                }\n            }\n            catch (FileNotFoundException)\n            {\n                _loggingType = LoggingType.File;\n                _logFolder = \"logs\";\n                _maxLogFileDays = 365;\n                _useLocalTime = false;\n\n                SaveConfigFileInternal();\n            }\n            catch (Exception ex)\n            {\n                //log to console since logger failed to load\n                Console.Write(ex.ToString());\n                return;\n            }\n\n            ApplyMaxLogFileDays();\n            ApplyLoggingType();\n        }\n\n        public void LoadConfig(Stream s)\n        {\n            lock (_saveLock)\n            {\n                try\n                {\n                    ReadConfigFrom(s);\n                }\n                catch (Exception ex)\n                {\n                    //log to console since logger failed to load\n                    Console.Write(ex.ToString());\n                    return;\n                }\n\n                //apply config changes\n                ApplyMaxLogFileDays();\n                ApplyLoggingType();\n\n                //save config file\n                SaveConfigFileInternal();\n\n                if (_pendingSave)\n                {\n                    _pendingSave = false;\n                    _saveTimer.Change(Timeout.Infinite, Timeout.Infinite);\n                }\n            }\n        }\n\n        private void SaveConfigFileInternal()\n        {\n            string logConfigFile = Path.Combine(_configFolder, \"log.config\");\n\n            using (MemoryStream mS = new MemoryStream())\n            {\n                //serialize config\n                WriteConfigTo(mS);\n\n                //write config\n                mS.Position = 0;\n\n                using (FileStream fS = new FileStream(logConfigFile, FileMode.Create, FileAccess.Write))\n                {\n                    mS.CopyTo(fS);\n                }\n            }\n\n            Write(\"DNS Server log config file was saved: \" + logConfigFile);\n        }\n\n        public void SaveConfigFile()\n        {\n            lock (_saveLock)\n            {\n                if (_pendingSave)\n                    return;\n\n                _pendingSave = true;\n                _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);\n            }\n        }\n\n        private void ReadConfigFrom(Stream s)\n        {\n            BinaryReader bR = new BinaryReader(s);\n\n            if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"LS\") //format\n                throw new InvalidDataException(\"DnsServer log config file format is invalid.\");\n\n            byte version = bR.ReadByte();\n            switch (version)\n            {\n                case 1:\n                    _loggingType = (LoggingType)bR.ReadByte();\n                    _logFolder = bR.ReadShortString();\n                    _maxLogFileDays = bR.ReadInt32();\n                    _useLocalTime = bR.ReadBoolean();\n                    break;\n\n                default:\n                    throw new InvalidDataException(\"DnsServer log config version not supported.\");\n            }\n        }\n\n        private void WriteConfigTo(Stream s)\n        {\n            BinaryWriter bW = new BinaryWriter(s);\n\n            bW.Write(Encoding.ASCII.GetBytes(\"LS\")); //format\n            bW.Write((byte)1); //version\n\n            bW.Write((byte)_loggingType);\n            bW.WriteShortString(_logFolder);\n            bW.Write(_maxLogFileDays);\n            bW.Write(_useLocalTime);\n        }\n\n        #endregion\n\n        #region private\n\n        private void StartLogging()\n        {\n            if (_isRunning)\n                return;\n\n            UnboundedChannelOptions options = new UnboundedChannelOptions();\n            options.SingleReader = true;\n\n            _channel = Channel.CreateUnbounded<LogQueueItem>(options);\n            _channelWriter = _channel.Writer;\n\n            if (_loggingType.HasFlag(LoggingType.File))\n            {\n                lock (_logFileLock)\n                {\n                    StartNewLogFile();\n                }\n            }\n\n            _isRunning = true;\n\n            //start consumer thread\n            _consumerThread = new Thread(async delegate ()\n            {\n                try\n                {\n                    await foreach (LogQueueItem item in _channel.Reader.ReadAllAsync())\n                    {\n                        if (!_isRunning)\n                            break;\n\n                        DateTime dateTime = _useLocalTime ? item._dateTime.ToLocalTime() : item._dateTime;\n                        string logEntry = GetLogEntry(dateTime, item._message);\n\n                        if (_loggingType.HasFlag(LoggingType.Console))\n                            Console.WriteLine(logEntry);\n\n                        if (_loggingType.HasFlag(LoggingType.File))\n                        {\n                            lock (_logFileLock)\n                            {\n                                if (dateTime.Date > _logDate)\n                                    StartNewLogFile();\n\n                                WriteLogEntry(logEntry);\n                            }\n                        }\n                    }\n                }\n                catch (Exception ex)\n                {\n                    Console.WriteLine(ex.ToString());\n                }\n            });\n\n            _consumerThread.Name = \"Log\";\n            _consumerThread.IsBackground = true;\n            _consumerThread.Start();\n        }\n\n        private void StopLogging()\n        {\n            if (!_isRunning)\n                return;\n\n            _channelWriter?.TryComplete();\n\n            lock (_logFileLock)\n            {\n                CloseCurrentLogFile();\n            }\n\n            _isRunning = false;\n        }\n\n        internal void BulkManipulateLogFiles(Action action)\n        {\n            lock (_logFileLock)\n            {\n                CloseCurrentLogFile();\n\n                try\n                {\n                    action();\n                }\n                finally\n                {\n                    if (_loggingType.HasFlag(LoggingType.File))\n                        StartNewLogFile();\n                }\n            }\n        }\n\n        private void ApplyLoggingType()\n        {\n            if (_isRunning)\n            {\n                //running\n                if (_loggingType == LoggingType.None)\n                {\n                    //no logging enabled\n                    StopLogging();\n                }\n                else if (_loggingType.HasFlag(LoggingType.File))\n                {\n                    //file logging is enabled\n                    if ((_logWriter is null) || !_logFile.StartsWith(ConvertToAbsolutePath(_logFolder)))\n                    {\n                        //file not being logged or log folder changed; start new log file\n                        lock (_logFileLock)\n                        {\n                            StartNewLogFile();\n                        }\n                    }\n                }\n                else\n                {\n                    //only console logging enabled; close open log file, if any\n                    if (_logWriter is not null)\n                    {\n                        lock (_logFileLock)\n                        {\n                            CloseCurrentLogFile();\n                        }\n                    }\n                }\n            }\n            else\n            {\n                //stopped\n                if (_loggingType != LoggingType.None)\n                    StartLogging();\n            }\n        }\n\n        private void ApplyLogFolder()\n        {\n            Directory.CreateDirectory(ConvertToAbsolutePath(_logFolder));\n\n            if (_loggingType.HasFlag(LoggingType.File))\n            {\n                lock (_logFileLock)\n                {\n                    StartNewLogFile();\n                }\n            }\n        }\n\n        private void ApplyMaxLogFileDays()\n        {\n            if (_maxLogFileDays > 0)\n                _logCleanupTimer.Change(LOG_CLEANUP_TIMER_INITIAL_INTERVAL, LOG_CLEANUP_TIMER_PERIODIC_INTERVAL);\n            else\n                _logCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);\n        }\n\n        private string ConvertToRelativePath(string path)\n        {\n            if (path.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))\n                path = path.Substring(_configFolder.Length).TrimStart(Path.DirectorySeparatorChar);\n\n            return path;\n        }\n\n        private string ConvertToAbsolutePath(string path)\n        {\n            if (Path.IsPathRooted(path))\n                return path;\n\n            return Path.Combine(_configFolder, path);\n        }\n\n        private void StartNewLogFile()\n        {\n            CloseCurrentLogFile();\n\n            string logFolder = ConvertToAbsolutePath(_logFolder);\n\n            if (!Directory.Exists(logFolder))\n                Directory.CreateDirectory(logFolder);\n\n            DateTime logStartDateTime = _useLocalTime ? DateTime.Now : DateTime.UtcNow;\n\n            _logFile = Path.Combine(logFolder, logStartDateTime.ToString(LOG_FILE_DATE_TIME_FORMAT) + \".log\");\n            _logWriter = new StreamWriter(new FileStream(_logFile, FileMode.Append, FileAccess.Write, FileShare.Read));\n            _logDate = logStartDateTime.Date;\n\n            WriteLogEntry(GetLogEntry(logStartDateTime, \"Logging started.\"));\n        }\n\n        private void CloseCurrentLogFile()\n        {\n            if (_logWriter is not null)\n            {\n                WriteLogEntry(GetLogEntry(DateTime.UtcNow, \"Logging stopped.\"));\n\n                _logWriter.Dispose();\n                _logWriter = null;\n            }\n        }\n\n        private string GetLogEntry(DateTime dateTime, string message)\n        {\n            string logEntry;\n\n            if (_useLocalTime)\n            {\n                if (dateTime.Kind == DateTimeKind.Local)\n                    logEntry = \"[\" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + \" Local] \" + message;\n                else\n                    logEntry = \"[\" + dateTime.ToLocalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + \" Local] \" + message;\n            }\n            else\n            {\n                if (dateTime.Kind == DateTimeKind.Utc)\n                    logEntry = \"[\" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + \" UTC] \" + message;\n                else\n                    logEntry = \"[\" + dateTime.ToUniversalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + \" UTC] \" + message;\n            }\n\n            return logEntry;\n        }\n\n        private void WriteLogEntry(string logEntry)\n        {\n            if (_logWriter is not null)\n            {\n                _logWriter.WriteLine(logEntry);\n                _logWriter.Flush();\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public string[] ListLogFiles()\n        {\n            return Directory.GetFiles(ConvertToAbsolutePath(_logFolder), \"*.log\", SearchOption.TopDirectoryOnly);\n        }\n\n        public async Task DownloadLogFileAsync(HttpContext context, string logName, long limit)\n        {\n            string logFileName = logName + \".log\";\n\n            using (FileStream fS = new FileStream(Path.Combine(ConvertToAbsolutePath(_logFolder), logFileName), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 64 * 1024, true))\n            {\n                HttpResponse response = context.Response;\n\n                response.ContentType = \"text/plain\";\n                response.Headers.ContentDisposition = \"attachment;filename=\" + logFileName;\n\n                if ((limit > fS.Length) || (limit < 1))\n                    limit = fS.Length;\n\n                OffsetStream oFS = new OffsetStream(fS, 0, limit);\n                HttpRequest request = context.Request;\n                Stream s;\n\n                string acceptEncoding = request.Headers.AcceptEncoding;\n                if (string.IsNullOrEmpty(acceptEncoding))\n                {\n                    s = response.Body;\n                }\n                else\n                {\n                    string[] acceptEncodingParts = acceptEncoding.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n\n                    if (acceptEncodingParts.Contains(\"br\"))\n                    {\n                        response.Headers.ContentEncoding = \"br\";\n                        s = new BrotliStream(response.Body, CompressionMode.Compress);\n                    }\n                    else if (acceptEncodingParts.Contains(\"gzip\"))\n                    {\n                        response.Headers.ContentEncoding = \"gzip\";\n                        s = new GZipStream(response.Body, CompressionMode.Compress);\n                    }\n                    else if (acceptEncodingParts.Contains(\"deflate\"))\n                    {\n                        response.Headers.ContentEncoding = \"deflate\";\n                        s = new DeflateStream(response.Body, CompressionMode.Compress);\n                    }\n                    else\n                    {\n                        s = response.Body;\n                    }\n                }\n\n                await using (s)\n                {\n                    await oFS.CopyToAsync(s);\n\n                    if (fS.Length > limit)\n                        await s.WriteAsync(Encoding.UTF8.GetBytes(\"\\r\\n####___TRUNCATED___####\"));\n                }\n            }\n        }\n\n        public void DeleteLogFile(string logName)\n        {\n            string logFile = Path.Combine(ConvertToAbsolutePath(_logFolder), logName + \".log\");\n\n            if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase))\n                DeleteCurrentLogFile();\n            else\n                File.Delete(logFile);\n        }\n\n        public void DeleteAllLogFiles()\n        {\n            string[] logFiles = ListLogFiles();\n\n            foreach (string logFile in logFiles)\n            {\n                if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase))\n                    DeleteCurrentLogFile();\n                else\n                    File.Delete(logFile);\n            }\n        }\n\n        public void Write(Exception ex)\n        {\n            Write(ex.ToString());\n        }\n\n        public void Write(IPEndPoint ep, Exception ex)\n        {\n            Write(ep, ex.ToString());\n        }\n\n        public void Write(IPEndPoint ep, string message)\n        {\n            string ipInfo;\n\n            if (ep == null)\n                ipInfo = \"\";\n            else if (ep.Address.IsIPv4MappedToIPv6)\n                ipInfo = \"[\" + ep.Address.MapToIPv4().ToString() + \":\" + ep.Port + \"] \";\n            else\n                ipInfo = \"[\" + ep.ToString() + \"] \";\n\n            Write(ipInfo + message);\n        }\n\n        public void Write(IPEndPoint ep, DnsTransportProtocol protocol, Exception ex)\n        {\n            Write(ep, protocol, ex.ToString());\n        }\n\n        public void Write(IPEndPoint ep, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response)\n        {\n            DnsQuestionRecord q = null;\n\n            if (request.Question.Count > 0)\n                q = request.Question[0];\n\n            string requestInfo;\n\n            if (q is null)\n                requestInfo = \"MISSING QUESTION!\";\n            else\n                requestInfo = \"QNAME: \" + q.Name + \"; QTYPE: \" + q.Type.ToString() + \"; QCLASS: \" + q.Class;\n\n            if (request.Additional.Count > 0)\n            {\n                DnsResourceRecord lastRR = request.Additional[request.Additional.Count - 1];\n\n                if ((lastRR.Type == DnsResourceRecordType.TSIG) && (lastRR.RDATA is DnsTSIGRecordData tsig))\n                    requestInfo += \"; TSIG KeyName: \" + lastRR.Name.ToLowerInvariant() + \"; TSIG Algo: \" + tsig.AlgorithmName + \"; TSIG Error: \" + tsig.Error.ToString();\n            }\n\n            string responseInfo;\n\n            if (response is null)\n            {\n                responseInfo = \"; NO RESPONSE FROM SERVER!\";\n            }\n            else\n            {\n                responseInfo = \"; RCODE: \" + response.RCODE.ToString();\n\n                string answer;\n\n                if (response.Answer.Count == 0)\n                {\n                    if (response.Truncation)\n                        answer = \"[TRUNCATED]\";\n                    else\n                        answer = \"[]\";\n                }\n                else if ((response.Answer.Count > 2) && response.IsZoneTransfer)\n                {\n                    answer = \"[ZONE TRANSFER]\";\n                }\n                else\n                {\n                    answer = \"[\";\n\n                    for (int i = 0; i < response.Answer.Count; i++)\n                    {\n                        if (i > 0)\n                            answer += \", \";\n\n                        answer += response.Answer[i].RDATA.ToString();\n                    }\n\n                    answer += \"]\";\n\n                    if (response.Additional.Count > 0)\n                    {\n                        switch (q.Type)\n                        {\n                            case DnsResourceRecordType.NS:\n                            case DnsResourceRecordType.MX:\n                            case DnsResourceRecordType.SRV:\n                                answer += \"; ADDITIONAL: [\";\n\n                                for (int i = 0; i < response.Additional.Count; i++)\n                                {\n                                    DnsResourceRecord additional = response.Additional[i];\n\n                                    switch (additional.Type)\n                                    {\n                                        case DnsResourceRecordType.A:\n                                        case DnsResourceRecordType.AAAA:\n                                            if (i > 0)\n                                                answer += \", \";\n\n                                            answer += additional.Name + \" (\" + additional.RDATA.ToString() + \")\";\n                                            break;\n                                    }\n                                }\n\n                                answer += \"]\";\n                                break;\n                        }\n                    }\n                }\n\n                EDnsClientSubnetOptionData responseECS = response.GetEDnsClientSubnetOption();\n                if (responseECS is not null)\n                    answer += \"; ECS: \" + responseECS.Address.ToString() + \"/\" + responseECS.ScopePrefixLength;\n\n                responseInfo += \"; ANSWER: \" + answer;\n            }\n\n            Write(ep, protocol, requestInfo + responseInfo);\n        }\n\n        public void Write(IPEndPoint ep, DnsTransportProtocol protocol, string message)\n        {\n            Write(ep, protocol.ToString(), message);\n        }\n\n        public void Write(IPEndPoint ep, string protocol, string message)\n        {\n            string ipInfo;\n\n            if (ep == null)\n                ipInfo = \"\";\n            else if (ep.Address.IsIPv4MappedToIPv6)\n                ipInfo = \"[\" + ep.Address.MapToIPv4().ToString() + \":\" + ep.Port + \"] \";\n            else\n                ipInfo = \"[\" + ep.ToString() + \"] \";\n\n            Write(ipInfo + \"[\" + protocol.ToUpper() + \"] \" + message);\n        }\n\n        public void Write(string message)\n        {\n            if (_loggingType != LoggingType.None)\n                _channelWriter?.TryWrite(new LogQueueItem(message));\n        }\n\n        public void DeleteCurrentLogFile()\n        {\n            lock (_logFileLock)\n            {\n                CloseCurrentLogFile();\n\n                File.Delete(_logFile);\n\n                if (_loggingType.HasFlag(LoggingType.File))\n                    StartNewLogFile();\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        public LoggingType LoggingType\n        {\n            get { return _loggingType; }\n            set\n            {\n                if (_loggingType != value)\n                {\n                    _loggingType = value;\n\n                    ApplyLoggingType();\n                }\n            }\n        }\n\n        public string LogFolder\n        {\n            get { return _logFolder; }\n            set\n            {\n                string logFolder;\n\n                if (string.IsNullOrEmpty(value))\n                    logFolder = \"logs\";\n                else if (value.Length > 255)\n                    throw new ArgumentException(\"Log folder path length cannot exceed 255 characters.\", nameof(LogFolder));\n                else\n                    logFolder = value;\n\n                string relativeLogFolder = ConvertToRelativePath(logFolder);\n\n                if (!relativeLogFolder.Equals(_logFolder, StringComparison.Ordinal))\n                {\n                    _logFolder = relativeLogFolder;\n\n                    ApplyLogFolder();\n                }\n            }\n        }\n\n        public int MaxLogFileDays\n        {\n            get { return _maxLogFileDays; }\n            set\n            {\n                if (value < 0)\n                    throw new ArgumentOutOfRangeException(nameof(MaxLogFileDays), \"MaxLogFileDays must be greater than or equal to 0.\");\n\n                if (_maxLogFileDays != value)\n                {\n                    _maxLogFileDays = value;\n\n                    ApplyMaxLogFileDays();\n                }\n            }\n        }\n\n        public bool UseLocalTime\n        {\n            get { return _useLocalTime; }\n            set { _useLocalTime = value; }\n        }\n\n        public string CurrentLogFile\n        { get { return _logFile; } }\n\n        public string LogFolderAbsolutePath\n        { get { return ConvertToAbsolutePath(_logFolder); } }\n\n        #endregion\n\n        readonly struct LogQueueItem\n        {\n            #region variables\n\n            public readonly DateTime _dateTime;\n            public readonly string _message;\n\n            #endregion\n\n            #region constructor\n\n            public LogQueueItem(string message)\n            {\n                _dateTime = DateTime.UtcNow;\n                _message = message;\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/TwoFactorAuthRequiredWebServiceException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore\n{\n    public class TwoFactorAuthRequiredWebServiceException : InvalidTokenWebServiceException\n    {\n        #region constructors\n\n        public TwoFactorAuthRequiredWebServiceException()\n            : base()\n        { }\n\n        public TwoFactorAuthRequiredWebServiceException(string message)\n            : base(message)\n        { }\n\n        public TwoFactorAuthRequiredWebServiceException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Dns;\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.Zones;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Http.Client;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        class WebServiceApi\n        {\n            #region variables\n\n            static readonly char[] _domainTrimChars = new char[] { '\\t', ' ', '.' };\n\n            readonly DnsWebService _dnsWebService;\n            readonly Uri _updateCheckUri;\n\n            string _checkForUpdateJsonData;\n            DateTime _checkForUpdateJsonDataUpdatedOn;\n            const int CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS = 3600;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceApi(DnsWebService dnsWebService, Uri updateCheckUri)\n            {\n                _dnsWebService = dnsWebService;\n                _updateCheckUri = updateCheckUri;\n            }\n\n            #endregion\n\n            #region private\n\n            private async Task<string> GetCheckForUpdateJsonData()\n            {\n                if ((_checkForUpdateJsonData is null) || (DateTime.UtcNow > _checkForUpdateJsonDataUpdatedOn.AddSeconds(CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS)))\n                {\n                    HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n                    handler.Proxy = _dnsWebService._dnsServer.Proxy;\n                    handler.NetworkType = _dnsWebService._dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n                    handler.DnsClient = _dnsWebService._dnsServer;\n\n                    using (HttpClient http = new HttpClient(handler))\n                    {\n                        _checkForUpdateJsonData = await http.GetStringAsync(_updateCheckUri);\n                        _checkForUpdateJsonDataUpdatedOn = DateTime.UtcNow;\n                    }\n                }\n\n                return _checkForUpdateJsonData;\n            }\n\n            #endregion\n\n            #region public\n\n            public async Task CheckForUpdateAsync(HttpContext context)\n            {\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                if (_updateCheckUri is null)\n                {\n                    jsonWriter.WriteBoolean(\"updateAvailable\", false);\n                    return;\n                }\n\n                try\n                {\n                    string jsonData = await GetCheckForUpdateJsonData();\n                    using JsonDocument jsonDocument = JsonDocument.Parse(jsonData);\n                    JsonElement jsonResponse = jsonDocument.RootElement;\n\n                    string updateVersion = jsonResponse.GetProperty(\"updateVersion\").GetString();\n                    string updateTitle = jsonResponse.GetPropertyValue(\"updateTitle\", null);\n                    string updateMessage = jsonResponse.GetPropertyValue(\"updateMessage\", null);\n                    string downloadLink = jsonResponse.GetPropertyValue(\"downloadLink\", null);\n                    string instructionsLink = jsonResponse.GetPropertyValue(\"instructionsLink\", null);\n                    string changeLogLink = jsonResponse.GetPropertyValue(\"changeLogLink\", null);\n\n                    bool updateAvailable = new Version(updateVersion) > _dnsWebService._currentVersion;\n\n                    jsonWriter.WriteBoolean(\"updateAvailable\", updateAvailable);\n                    jsonWriter.WriteString(\"updateVersion\", updateVersion);\n                    jsonWriter.WriteString(\"currentVersion\", _dnsWebService.GetServerVersion());\n\n                    if (updateAvailable)\n                    {\n                        jsonWriter.WriteString(\"updateTitle\", updateTitle);\n                        jsonWriter.WriteString(\"updateMessage\", updateMessage);\n                        jsonWriter.WriteString(\"downloadLink\", downloadLink);\n                        jsonWriter.WriteString(\"instructionsLink\", instructionsLink);\n                        jsonWriter.WriteString(\"changeLogLink\", changeLogLink);\n                    }\n\n                    string strLog = \"Check for update was done {updateAvailable: \" + updateAvailable + \"; updateVersion: \" + updateVersion + \";\";\n\n                    if (!string.IsNullOrEmpty(updateTitle))\n                        strLog += \" updateTitle: \" + updateTitle + \";\";\n\n                    if (!string.IsNullOrEmpty(updateMessage))\n                        strLog += \" updateMessage: \" + updateMessage + \";\";\n\n                    if (!string.IsNullOrEmpty(downloadLink))\n                        strLog += \" downloadLink: \" + downloadLink + \";\";\n\n                    if (!string.IsNullOrEmpty(instructionsLink))\n                        strLog += \" instructionsLink: \" + instructionsLink + \";\";\n\n                    if (!string.IsNullOrEmpty(changeLogLink))\n                        strLog += \" changeLogLink: \" + changeLogLink + \";\";\n\n                    strLog += \"}\";\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), strLog);\n                }\n                catch (Exception ex)\n                {\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"Check for update was done {updateAvailable: False;}\\r\\n\" + ex.ToString());\n\n                    jsonWriter.WriteBoolean(\"updateAvailable\", false);\n                }\n            }\n\n            public async Task ResolveQueryAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DnsClient, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string server = request.GetQueryOrForm(\"server\");\n                string domain = request.GetQueryOrForm(\"domain\").Trim(_domainTrimChars);\n                DnsResourceRecordType type = request.GetQueryOrFormEnum<DnsResourceRecordType>(\"type\");\n                DnsTransportProtocol protocol = request.GetQueryOrFormEnum(\"protocol\", DnsTransportProtocol.Udp);\n                bool dnssecValidation = request.GetQueryOrForm(\"dnssec\", bool.Parse, false);\n\n                NetworkAddress eDnsClientSubnet = request.GetQueryOrForm(\"eDnsClientSubnet\", NetworkAddress.Parse, null);\n                if (eDnsClientSubnet is not null)\n                {\n                    switch (eDnsClientSubnet.AddressFamily)\n                    {\n                        case AddressFamily.InterNetwork:\n                            if (eDnsClientSubnet.PrefixLength == 32)\n                                eDnsClientSubnet = new NetworkAddress(eDnsClientSubnet.Address, 24);\n\n                            break;\n\n                        case AddressFamily.InterNetworkV6:\n                            if (eDnsClientSubnet.PrefixLength == 128)\n                                eDnsClientSubnet = new NetworkAddress(eDnsClientSubnet.Address, 56);\n\n                            break;\n                    }\n                }\n\n                bool importResponse = request.GetQueryOrForm(\"import\", bool.Parse, false);\n                NetProxy proxy = _dnsWebService._dnsServer.Proxy;\n                bool preferIPv6 = _dnsWebService._dnsServer.PreferIPv6;\n                ushort udpPayloadSize = _dnsWebService._dnsServer.UdpPayloadSize;\n                bool randomizeName = false;\n                bool qnameMinimization = _dnsWebService._dnsServer.QnameMinimization;\n                const int RETRIES = 1;\n                const int TIMEOUT = 10000;\n\n                DnsDatagram dnsResponse;\n                List<DnsDatagram> rawResponses = new List<DnsDatagram>();\n                string dnssecErrorMessage = null;\n\n                if (server.Equals(\"recursive-resolver\", StringComparison.OrdinalIgnoreCase))\n                {\n                    if (type == DnsResourceRecordType.AXFR)\n                        throw new DnsServerException(\"Cannot do zone transfer (AXFR) for 'recursive-resolver'.\");\n\n                    DnsQuestionRecord question;\n\n                    if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress address))\n                        question = new DnsQuestionRecord(address, DnsClass.IN);\n                    else\n                        question = new DnsQuestionRecord(domain, type, DnsClass.IN);\n\n                    DnsCache dnsCache = new DnsCache();\n                    dnsCache.MinimumRecordTtl = 0;\n                    dnsCache.MaximumRecordTtl = 7 * 24 * 60 * 60;\n\n                    try\n                    {\n                        dnsResponse = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1)\n                        {\n                            return await DnsClient.RecursiveResolveAsync(question, dnsCache, proxy, preferIPv6, udpPayloadSize, randomizeName, qnameMinimization, dnssecValidation, eDnsClientSubnet, RETRIES, TIMEOUT, rawResponses: rawResponses, cancellationToken: cancellationToken1);\n                        }, DnsServer.RECURSIVE_RESOLUTION_TIMEOUT);\n                    }\n                    catch (DnsClientResponseDnssecValidationException ex)\n                    {\n                        if (ex.InnerException is DnsClientResponseDnssecValidationException ex1)\n                            ex = ex1;\n\n                        dnsResponse = ex.Response;\n                        dnssecErrorMessage = ex.Message;\n                        importResponse = false;\n                    }\n                }\n                else if (server.Equals(\"system-dns\", StringComparison.OrdinalIgnoreCase))\n                {\n                    DnsClient dnsClient = new DnsClient();\n\n                    dnsClient.Proxy = proxy;\n                    dnsClient.PreferIPv6 = preferIPv6;\n                    dnsClient.RandomizeName = randomizeName;\n                    dnsClient.Retries = RETRIES;\n                    dnsClient.Timeout = TIMEOUT;\n                    dnsClient.UdpPayloadSize = udpPayloadSize;\n                    dnsClient.DnssecValidation = dnssecValidation;\n                    dnsClient.EDnsClientSubnet = eDnsClientSubnet;\n\n                    try\n                    {\n                        dnsResponse = await dnsClient.ResolveAsync(domain, type);\n                    }\n                    catch (DnsClientResponseDnssecValidationException ex)\n                    {\n                        if (ex.InnerException is DnsClientResponseDnssecValidationException ex1)\n                            ex = ex1;\n\n                        dnsResponse = ex.Response;\n                        dnssecErrorMessage = ex.Message;\n                        importResponse = false;\n                    }\n                }\n                else\n                {\n                    if ((type == DnsResourceRecordType.AXFR) && (protocol == DnsTransportProtocol.Udp))\n                        protocol = DnsTransportProtocol.Tcp;\n\n                    NameServerAddress nameServer;\n\n                    if (server.Equals(\"this-server\", StringComparison.OrdinalIgnoreCase))\n                    {\n                        switch (protocol)\n                        {\n                            case DnsTransportProtocol.Udp:\n                                nameServer = _dnsWebService._dnsServer.ThisServer;\n                                break;\n\n                            case DnsTransportProtocol.Tcp:\n                                nameServer = _dnsWebService._dnsServer.ThisServer.Clone(DnsTransportProtocol.Tcp);\n                                break;\n\n                            case DnsTransportProtocol.Tls:\n                                throw new DnsServerException(\"Cannot use DNS-over-TLS protocol for 'this-server'. Please use the TLS certificate domain name as the server.\");\n\n                            case DnsTransportProtocol.Https:\n                                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.\");\n\n                            case DnsTransportProtocol.Quic:\n                                throw new DnsServerException(\"Cannot use DNS-over-QUIC protocol for 'this-server'. Please use the TLS certificate domain name as the server.\");\n\n                            default:\n                                throw new NotSupportedException(\"DNS transport protocol is not supported: \" + protocol.ToString());\n                        }\n\n                        proxy = null; //no proxy required for this server\n                    }\n                    else\n                    {\n                        nameServer = NameServerAddress.Parse(server);\n\n                        if (nameServer.Protocol != protocol)\n                            nameServer = nameServer.Clone(protocol);\n\n                        if (nameServer.IsIPEndPointStale)\n                            await nameServer.ResolveIPAddressAsync(_dnsWebService._dnsServer, _dnsWebService._dnsServer.PreferIPv6);\n\n                        if ((nameServer.DomainEndPoint is null) && ((protocol == DnsTransportProtocol.Udp) || (protocol == DnsTransportProtocol.Tcp)))\n                        {\n                            try\n                            {\n                                await nameServer.ResolveDomainNameAsync(_dnsWebService._dnsServer);\n                            }\n                            catch\n                            { }\n                        }\n                    }\n\n                    DnsClient dnsClient = new DnsClient(nameServer);\n\n                    dnsClient.Proxy = proxy;\n                    dnsClient.PreferIPv6 = preferIPv6;\n                    dnsClient.RandomizeName = randomizeName;\n                    dnsClient.Retries = RETRIES;\n                    dnsClient.Timeout = TIMEOUT;\n                    dnsClient.UdpPayloadSize = udpPayloadSize;\n                    dnsClient.DnssecValidation = dnssecValidation;\n                    dnsClient.EDnsClientSubnet = eDnsClientSubnet;\n\n                    if (dnssecValidation)\n                    {\n                        if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress ptrIp))\n                            domain = ptrIp.GetReverseDomain();\n\n                        //load trust anchors into dns client if domain is locally hosted\n                        _dnsWebService._dnsServer.AuthZoneManager.LoadTrustAnchorsTo(dnsClient, domain, type);\n                    }\n\n                    try\n                    {\n                        dnsResponse = await dnsClient.ResolveAsync(domain, type);\n                    }\n                    catch (DnsClientResponseDnssecValidationException ex)\n                    {\n                        if (ex.InnerException is DnsClientResponseDnssecValidationException ex1)\n                            ex = ex1;\n\n                        dnsResponse = ex.Response;\n                        dnssecErrorMessage = ex.Message;\n                        importResponse = false;\n                    }\n\n                    if (type == DnsResourceRecordType.AXFR)\n                        dnsResponse = dnsResponse.Join();\n                }\n\n                if (importResponse)\n                {\n                    bool isZoneImport = false;\n\n                    if (type == DnsResourceRecordType.AXFR)\n                    {\n                        isZoneImport = true;\n                    }\n                    else\n                    {\n                        foreach (DnsResourceRecord record in dnsResponse.Answer)\n                        {\n                            if (record.Type == DnsResourceRecordType.SOA)\n                            {\n                                if (record.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                                    isZoneImport = true;\n\n                                break;\n                            }\n                        }\n                    }\n\n                    AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(domain);\n                    if (\n                        (zoneInfo is null) ||\n                        ((zoneInfo.Type != AuthZoneType.Primary) && (zoneInfo.Type != AuthZoneType.Forwarder) && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) ||\n                        (isZoneImport && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                       )\n                    {\n                        if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                            throw new DnsWebServiceException(\"Access was denied.\");\n\n                        zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(domain);\n                        if (zoneInfo is null)\n                            throw new DnsServerException(\"Cannot import records: failed to create primary zone.\");\n\n                        //set permissions\n                        _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                        _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                        _dnsWebService._authManager.SaveConfigFile();\n                    }\n                    else\n                    {\n                        if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                            throw new DnsWebServiceException(\"Access was denied.\");\n\n                        switch (zoneInfo.Type)\n                        {\n                            case AuthZoneType.Primary:\n                                break;\n\n                            case AuthZoneType.Forwarder:\n                                if (type == DnsResourceRecordType.AXFR)\n                                    throw new DnsServerException(\"Cannot import records via zone transfer: import zone must be of primary type.\");\n\n                                break;\n\n                            default:\n                                throw new DnsServerException(\"Cannot import records: import zone must be of primary or forwarder type.\");\n                        }\n                    }\n\n                    if (type == DnsResourceRecordType.AXFR)\n                    {\n                        _dnsWebService._dnsServer.AuthZoneManager.SyncZoneTransferRecords(zoneInfo.Name, dnsResponse.Answer);\n                    }\n                    else\n                    {\n                        List<DnsResourceRecord> importRecords = new List<DnsResourceRecord>(dnsResponse.Answer.Count + dnsResponse.Authority.Count);\n\n                        foreach (DnsResourceRecord record in dnsResponse.Answer)\n                        {\n                            if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith(\".\" + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0))\n                            {\n                                record.RemoveExpiry();\n                                record.Tag = null; //remove cache zone record info\n\n                                importRecords.Add(record);\n\n                                if (record.Type == DnsResourceRecordType.NS)\n                                    record.SyncGlueRecords(dnsResponse.Additional);\n                            }\n                        }\n\n                        foreach (DnsResourceRecord record in dnsResponse.Authority)\n                        {\n                            if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith(\".\" + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0))\n                            {\n                                record.RemoveExpiry();\n                                record.Tag = null; //remove cache zone record info\n\n                                importRecords.Add(record);\n\n                                if (record.Type == DnsResourceRecordType.NS)\n                                    record.SyncGlueRecords(dnsResponse.Additional);\n                            }\n                        }\n\n                        _dnsWebService._dnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, importRecords, true, true);\n                    }\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS Client imported record(s) for authoritative zone {server: \" + server + \"; zone: \" + zoneInfo.DisplayName + \"; type: \" + type + \";}\");\n                }\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                if (dnssecErrorMessage is not null)\n                    jsonWriter.WriteString(\"warningMessage\", dnssecErrorMessage);\n\n                jsonWriter.WritePropertyName(\"result\");\n                dnsResponse.SerializeTo(jsonWriter);\n\n                jsonWriter.WritePropertyName(\"rawResponses\");\n                jsonWriter.WriteStartArray();\n\n                for (int i = 0; i < rawResponses.Count; i++)\n                    rawResponses[i].SerializeTo(jsonWriter);\n\n                jsonWriter.WriteEndArray();\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceAppsApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing DnsServerCore.Auth;\nusing DnsServerCore.Dns.Applications;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        sealed class WebServiceAppsApi \n        {\n            #region variables\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceAppsApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region private\n\n            private void WriteAppAsJson(Utf8JsonWriter jsonWriter, DnsApplication application, JsonElement jsonStoreAppsArray = default)\n            {\n                jsonWriter.WriteStartObject();\n\n                jsonWriter.WriteString(\"name\", application.Name);\n                jsonWriter.WriteString(\"description\", application.Description);\n                jsonWriter.WriteString(\"version\", DnsWebService.GetCleanVersion(application.Version));\n\n                if (jsonStoreAppsArray.ValueKind != JsonValueKind.Undefined)\n                {\n                    foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray())\n                    {\n                        string name = jsonStoreApp.GetProperty(\"name\").GetString();\n                        if (name.Equals(application.Name))\n                        {\n                            string version = null;\n                            string url = null;\n                            Version storeAppVersion = null;\n                            Version lastServerVersion = null;\n\n                            foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty(\"versions\").EnumerateArray())\n                            {\n                                string strServerVersion = jsonVersion.GetProperty(\"serverVersion\").GetString();\n                                Version requiredServerVersion = new Version(strServerVersion);\n\n                                if (_dnsWebService._currentVersion < requiredServerVersion)\n                                    continue;\n\n                                if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion))\n                                    continue;\n\n                                version = jsonVersion.GetProperty(\"version\").GetString();\n                                url = jsonVersion.GetProperty(\"url\").GetString();\n\n                                storeAppVersion = new Version(version);\n                                lastServerVersion = requiredServerVersion;\n                            }\n\n                            if (storeAppVersion is null)\n                                break; //no compatible update available\n\n                            jsonWriter.WriteString(\"updateVersion\", version);\n                            jsonWriter.WriteString(\"updateUrl\", url);\n                            jsonWriter.WriteBoolean(\"updateAvailable\", storeAppVersion > application.Version);\n                            break;\n                        }\n                    }\n                }\n\n                jsonWriter.WritePropertyName(\"dnsApps\");\n                {\n                    jsonWriter.WriteStartArray();\n\n                    foreach (KeyValuePair<string, IDnsApplication> dnsApp in application.DnsApplications)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"classPath\", dnsApp.Key);\n                        jsonWriter.WriteString(\"description\", dnsApp.Value.Description);\n\n                        if (dnsApp.Value is IDnsAppRecordRequestHandler appRecordHandler)\n                        {\n                            jsonWriter.WriteBoolean(\"isAppRecordRequestHandler\", true);\n                            jsonWriter.WriteString(\"recordDataTemplate\", appRecordHandler.ApplicationRecordDataTemplate);\n                        }\n                        else\n                        {\n                            jsonWriter.WriteBoolean(\"isAppRecordRequestHandler\", false);\n                        }\n\n                        jsonWriter.WriteBoolean(\"isRequestController\", dnsApp.Value is IDnsRequestController);\n                        jsonWriter.WriteBoolean(\"isAuthoritativeRequestHandler\", dnsApp.Value is IDnsAuthoritativeRequestHandler);\n                        jsonWriter.WriteBoolean(\"isRequestBlockingHandler\", dnsApp.Value is IDnsRequestBlockingHandler);\n                        jsonWriter.WriteBoolean(\"isQueryLogger\", dnsApp.Value is IDnsQueryLogger);\n                        jsonWriter.WriteBoolean(\"isQueryLogs\", dnsApp.Value is IDnsQueryLogs);\n                        jsonWriter.WriteBoolean(\"isPostProcessor\", dnsApp.Value is IDnsPostProcessor);\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                jsonWriter.WriteEndObject();\n            }\n\n            #endregion\n\n            #region public\n\n            public async Task ListInstalledAppsAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (\n                    !_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.View) &&\n                    !_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View) &&\n                    !_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View)\n                   )\n                {\n                    throw new DnsWebServiceException(\"Access was denied.\");\n                }\n\n                List<string> apps = new List<string>(_dnsWebService._dnsServer.DnsApplicationManager.Applications.Keys);\n                apps.Sort();\n\n                JsonDocument jsonDocument = null;\n                try\n                {\n                    JsonElement jsonStoreAppsArray = default;\n\n                    if (apps.Count > 0)\n                    {\n                        try\n                        {\n                            string storeAppsJsonData = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                            {\n                                return _dnsWebService._dnsServer.DnsApplicationManager.GetStoreAppsJsonData();\n                            }, 5000);\n\n                            jsonDocument = JsonDocument.Parse(storeAppsJsonData);\n                            jsonStoreAppsArray = jsonDocument.RootElement;\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(ex);\n                        }\n                    }\n\n                    Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                    jsonWriter.WritePropertyName(\"apps\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (string app in apps)\n                    {\n                        if (_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(app, out DnsApplication application))\n                            WriteAppAsJson(jsonWriter, application, jsonStoreAppsArray);\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n                finally\n                {\n                    if (jsonDocument is not null)\n                        jsonDocument.Dispose();\n                }\n            }\n\n            public async Task ListStoreApps(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string storeAppsJsonData = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                {\n                    return _dnsWebService._dnsServer.DnsApplicationManager.GetStoreAppsJsonData();\n                }, 30000);\n\n                using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData);\n                JsonElement jsonStoreAppsArray = jsonDocument.RootElement;\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"storeApps\");\n                jsonWriter.WriteStartArray();\n\n                foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray())\n                {\n                    string name = jsonStoreApp.GetProperty(\"name\").GetString();\n                    string description = jsonStoreApp.GetProperty(\"description\").GetString();\n                    string version = null;\n                    string url = null;\n                    string size = null;\n                    Version storeAppVersion = null;\n                    Version lastServerVersion = null;\n\n                    foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty(\"versions\").EnumerateArray())\n                    {\n                        string strServerVersion = jsonVersion.GetProperty(\"serverVersion\").GetString();\n                        Version requiredServerVersion = new Version(strServerVersion);\n\n                        if (_dnsWebService._currentVersion < requiredServerVersion)\n                            continue;\n\n                        if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion))\n                            continue;\n\n                        version = jsonVersion.GetProperty(\"version\").GetString();\n                        url = jsonVersion.GetProperty(\"url\").GetString();\n                        size = jsonVersion.GetProperty(\"size\").GetString();\n\n                        storeAppVersion = new Version(version);\n                        lastServerVersion = requiredServerVersion;\n                    }\n\n                    if (storeAppVersion is null)\n                        continue; //app is not compatible\n\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"name\", name);\n                    jsonWriter.WriteString(\"description\", description);\n                    jsonWriter.WriteString(\"version\", version);\n                    jsonWriter.WriteString(\"url\", url);\n                    jsonWriter.WriteString(\"size\", size);\n\n                    bool installed = _dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication installedApp);\n\n                    jsonWriter.WriteBoolean(\"installed\", installed);\n\n                    if (installed)\n                    {\n                        jsonWriter.WriteString(\"installedVersion\", DnsWebService.GetCleanVersion(installedApp.Version));\n                        jsonWriter.WriteBoolean(\"updateAvailable\", storeAppVersion > installedApp.Version);\n                    }\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public async Task DownloadAndInstallAppAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\").Trim();\n                string url = request.GetQueryOrForm(\"url\");\n\n                if (!url.StartsWith(\"https://\", StringComparison.OrdinalIgnoreCase))\n                    throw new DnsWebServiceException(\"Parameter 'url' value must start with 'https://'.\");\n\n                DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.DownloadAndInstallAppAsync(name, new Uri(url));\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS application '\" + name + \"' was installed successfully from: \" + url);\n                \n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"installedApp\");\n                WriteAppAsJson(jsonWriter, application);\n            }\n\n            public async Task DownloadAndUpdateAppAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\").Trim();\n                string url = request.GetQueryOrForm(\"url\");\n\n                if (!url.StartsWith(\"https://\", StringComparison.OrdinalIgnoreCase))\n                    throw new DnsWebServiceException(\"Parameter 'url' value must start with 'https://'.\");\n\n                DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.DownloadAndUpdateAppAsync(name, new Uri(url));\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS application '\" + name + \"' was updated successfully from: \" + url);\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"updatedApp\");\n                WriteAppAsJson(jsonWriter, application);\n            }\n\n            public async Task InstallAppAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\").Trim();\n\n                if (!request.HasFormContentType || (request.Form.Files.Count == 0))\n                    throw new DnsWebServiceException(\"DNS application zip file is missing.\");\n\n                string tmpFile = Path.GetTempFileName();\n                try\n                {\n                    await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                    {\n                        //write to temp file\n                        await request.Form.Files[0].CopyToAsync(fS);\n\n                        //install app\n                        fS.Position = 0;\n                        DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.InstallApplicationAsync(name, fS);\n\n                        _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS application '\" + name + \"' was installed successfully.\");\n                        \n                        //trigger cluster update\n                        if (_dnsWebService._clusterManager.ClusterInitialized)\n                            _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                        Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                        jsonWriter.WritePropertyName(\"installedApp\");\n                        WriteAppAsJson(jsonWriter, application);\n                    }\n                }\n                finally\n                {\n                    try\n                    {\n                        File.Delete(tmpFile);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsWebService._log.Write(ex);\n                    }\n                }\n            }\n\n            public async Task UpdateAppAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\").Trim();\n\n                if (!request.HasFormContentType || (request.Form.Files.Count == 0))\n                    throw new DnsWebServiceException(\"DNS application zip file is missing.\");\n\n                string tmpFile = Path.GetTempFileName();\n                try\n                {\n                    await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                    {\n                        //write to temp file\n                        await request.Form.Files[0].CopyToAsync(fS);\n\n                        //update app\n                        fS.Position = 0;\n                        DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.UpdateApplicationAsync(name, fS);\n\n                        _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS application '\" + name + \"' was updated successfully.\");\n\n                        //trigger cluster update\n                        if (_dnsWebService._clusterManager.ClusterInitialized)\n                            _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n                        \n                        Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                        jsonWriter.WritePropertyName(\"updatedApp\");\n                        WriteAppAsJson(jsonWriter, application);\n                    }\n                }\n                finally\n                {\n                    try\n                    {\n                        File.Delete(tmpFile);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsWebService._log.Write(ex);\n                    }\n                }\n            }\n\n            public void UninstallApp(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\").Trim();\n\n                _dnsWebService._dnsServer.DnsApplicationManager.UninstallApplication(name);\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS application '\" + name + \"' was uninstalled successfully.\");\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public async Task GetAppConfigAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\").Trim();\n\n                if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application))\n                    throw new DnsWebServiceException(\"DNS application was not found: \" + name);\n\n                string config = await application.GetConfigAsync();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                jsonWriter.WriteString(\"config\", config);\n            }\n\n            public async Task SetAppConfigAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\").Trim();\n\n                if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application))\n                    throw new DnsWebServiceException(\"DNS application was not found: \" + name);\n\n                string config = request.QueryOrForm(\"config\");\n                if (config is null)\n                    throw new DnsWebServiceException(\"Parameter 'config' missing.\");\n\n                if (config.Length == 0)\n                    config = null;\n\n                await application.SetConfigAsync(config);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS application '\" + name + \"' app config was saved successfully.\");\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceAuthApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Security.OTP;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        sealed class WebServiceAuthApi\n        {\n            #region variables\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceAuthApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region private\n\n            private void WriteCurrentSessionDetails(Utf8JsonWriter jsonWriter, UserSession currentSession, bool includeInfo)\n            {\n                if (currentSession.Type == UserSessionType.ApiToken)\n                {\n                    jsonWriter.WriteString(\"username\", currentSession.User.Username);\n                    jsonWriter.WriteString(\"tokenName\", currentSession.TokenName);\n                    jsonWriter.WriteString(\"token\", currentSession.Token);\n                }\n                else\n                {\n                    jsonWriter.WriteString(\"displayName\", currentSession.User.DisplayName);\n                    jsonWriter.WriteString(\"username\", currentSession.User.Username);\n                    jsonWriter.WriteBoolean(\"totpEnabled\", currentSession.User.TOTPEnabled);\n                    jsonWriter.WriteString(\"token\", currentSession.Token);\n                }\n\n                if (includeInfo)\n                {\n                    jsonWriter.WriteStartObject(\"info\");\n\n                    jsonWriter.WriteString(\"version\", _dnsWebService.GetServerVersion());\n                    jsonWriter.WriteString(\"uptimestamp\", _dnsWebService._uptimestamp);\n                    jsonWriter.WriteString(\"dnsServerDomain\", _dnsWebService._dnsServer.ServerDomain);\n                    jsonWriter.WriteNumber(\"defaultRecordTtl\", _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl);\n                    jsonWriter.WriteNumber(\"defaultNsRecordTtl\", _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl);\n                    jsonWriter.WriteNumber(\"defaultSoaRecordTtl\", _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl);\n                    jsonWriter.WriteBoolean(\"useSoaSerialDateScheme\", _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme);\n                    jsonWriter.WriteBoolean(\"dnssecValidation\", _dnsWebService._dnsServer.DnssecValidation);\n\n                    jsonWriter.WriteBoolean(\"clusterInitialized\", _dnsWebService._clusterManager.ClusterInitialized);\n\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                    {\n                        jsonWriter.WriteString(\"clusterDomain\", _dnsWebService._clusterManager.ClusterDomain);\n\n                        _dnsWebService._clusterApi.WriteClusterNodes(jsonWriter);\n                    }\n\n                    jsonWriter.WriteStartObject(\"permissions\");\n\n                    for (int i = 1; i <= 11; i++)\n                    {\n                        PermissionSection section = (PermissionSection)i;\n\n                        jsonWriter.WritePropertyName(section.ToString());\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteBoolean(\"canView\", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.View));\n                        jsonWriter.WriteBoolean(\"canModify\", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.Modify));\n                        jsonWriter.WriteBoolean(\"canDelete\", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.Delete));\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndObject();\n\n                    jsonWriter.WriteEndObject();\n                }\n            }\n\n            private void WriteUserDetails(Utf8JsonWriter jsonWriter, User user, UserSession currentSession, bool includeMoreDetails, bool includeGroups)\n            {\n                jsonWriter.WriteString(\"displayName\", user.DisplayName);\n                jsonWriter.WriteString(\"username\", user.Username);\n                jsonWriter.WriteBoolean(\"totpEnabled\", user.TOTPEnabled);\n                jsonWriter.WriteBoolean(\"disabled\", user.Disabled);\n                jsonWriter.WriteString(\"previousSessionLoggedOn\", user.PreviousSessionLoggedOn);\n                jsonWriter.WriteString(\"previousSessionRemoteAddress\", user.PreviousSessionRemoteAddress.ToString());\n                jsonWriter.WriteString(\"recentSessionLoggedOn\", user.RecentSessionLoggedOn);\n                jsonWriter.WriteString(\"recentSessionRemoteAddress\", user.RecentSessionRemoteAddress.ToString());\n\n                if (includeMoreDetails)\n                {\n                    jsonWriter.WriteNumber(\"sessionTimeoutSeconds\", user.SessionTimeoutSeconds);\n\n                    jsonWriter.WritePropertyName(\"memberOfGroups\");\n                    jsonWriter.WriteStartArray();\n\n                    List<Group> memberOfGroups = new List<Group>(user.MemberOfGroups);\n                    memberOfGroups.Sort();\n\n                    foreach (Group group in memberOfGroups)\n                    {\n                        if (group.Name.Equals(\"Everyone\", StringComparison.OrdinalIgnoreCase))\n                            continue;\n\n                        jsonWriter.WriteStringValue(group.Name);\n                    }\n\n                    jsonWriter.WriteEndArray();\n\n                    jsonWriter.WritePropertyName(\"sessions\");\n                    jsonWriter.WriteStartArray();\n\n                    List<UserSession> sessions = _dnsWebService._authManager.GetSessions(user);\n                    sessions.Sort();\n\n                    foreach (UserSession session in sessions)\n                        WriteUserSessionDetails(jsonWriter, session, currentSession);\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (includeGroups)\n                {\n                    List<Group> groups = new List<Group>(_dnsWebService._authManager.Groups);\n                    groups.Sort();\n\n                    jsonWriter.WritePropertyName(\"groups\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (Group group in groups)\n                    {\n                        if (group.Name.Equals(\"Everyone\", StringComparison.OrdinalIgnoreCase))\n                            continue;\n\n                        jsonWriter.WriteStringValue(group.Name);\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n            }\n\n            private static void WriteUserSessionDetails(Utf8JsonWriter jsonWriter, UserSession session, UserSession currentSession)\n            {\n                jsonWriter.WriteStartObject();\n\n                jsonWriter.WriteString(\"username\", session.User.Username);\n                jsonWriter.WriteBoolean(\"isCurrentSession\", session.Equals(currentSession));\n                jsonWriter.WriteString(\"partialToken\", session.Token.AsSpan(0, 16));\n                jsonWriter.WriteString(\"type\", session.Type.ToString());\n                jsonWriter.WriteString(\"tokenName\", session.TokenName);\n                jsonWriter.WriteString(\"lastSeen\", session.LastSeen);\n                jsonWriter.WriteString(\"lastSeenRemoteAddress\", session.LastSeenRemoteAddress.ToString());\n                jsonWriter.WriteString(\"lastSeenUserAgent\", session.LastSeenUserAgent);\n\n                jsonWriter.WriteEndObject();\n            }\n\n            private void WriteGroupDetails(Utf8JsonWriter jsonWriter, Group group, bool includeMembers, bool includeUsers)\n            {\n                jsonWriter.WriteString(\"name\", group.Name);\n                jsonWriter.WriteString(\"description\", group.Description);\n\n                if (includeMembers)\n                {\n                    jsonWriter.WritePropertyName(\"members\");\n                    jsonWriter.WriteStartArray();\n\n                    List<User> members = _dnsWebService._authManager.GetGroupMembers(group);\n                    members.Sort();\n\n                    foreach (User user in members)\n                        jsonWriter.WriteStringValue(user.Username);\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (includeUsers)\n                {\n                    List<User> users = new List<User>(_dnsWebService._authManager.Users);\n                    users.Sort();\n\n                    jsonWriter.WritePropertyName(\"users\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (User user in users)\n                        jsonWriter.WriteStringValue(user.Username);\n\n                    jsonWriter.WriteEndArray();\n                }\n            }\n\n            private void WritePermissionDetails(Utf8JsonWriter jsonWriter, Permission permission, string subItem, bool includeUsersAndGroups)\n            {\n                jsonWriter.WriteString(\"section\", permission.Section.ToString());\n\n                if (subItem is not null)\n                    jsonWriter.WriteString(\"subItem\", subItem.Length == 0 ? \".\" : subItem);\n\n                jsonWriter.WritePropertyName(\"userPermissions\");\n                jsonWriter.WriteStartArray();\n\n                List<KeyValuePair<User, PermissionFlag>> userPermissions = new List<KeyValuePair<User, PermissionFlag>>(permission.UserPermissions);\n\n                userPermissions.Sort(delegate (KeyValuePair<User, PermissionFlag> x, KeyValuePair<User, PermissionFlag> y)\n                {\n                    return x.Key.Username.CompareTo(y.Key.Username);\n                });\n\n                foreach (KeyValuePair<User, PermissionFlag> userPermission in userPermissions)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"username\", userPermission.Key.Username);\n                    jsonWriter.WriteBoolean(\"canView\", userPermission.Value.HasFlag(PermissionFlag.View));\n                    jsonWriter.WriteBoolean(\"canModify\", userPermission.Value.HasFlag(PermissionFlag.Modify));\n                    jsonWriter.WriteBoolean(\"canDelete\", userPermission.Value.HasFlag(PermissionFlag.Delete));\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WritePropertyName(\"groupPermissions\");\n                jsonWriter.WriteStartArray();\n\n                List<KeyValuePair<Group, PermissionFlag>> groupPermissions = new List<KeyValuePair<Group, PermissionFlag>>(permission.GroupPermissions);\n\n                groupPermissions.Sort(delegate (KeyValuePair<Group, PermissionFlag> x, KeyValuePair<Group, PermissionFlag> y)\n                {\n                    return x.Key.Name.CompareTo(y.Key.Name);\n                });\n\n                foreach (KeyValuePair<Group, PermissionFlag> groupPermission in groupPermissions)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"name\", groupPermission.Key.Name);\n                    jsonWriter.WriteBoolean(\"canView\", groupPermission.Value.HasFlag(PermissionFlag.View));\n                    jsonWriter.WriteBoolean(\"canModify\", groupPermission.Value.HasFlag(PermissionFlag.Modify));\n                    jsonWriter.WriteBoolean(\"canDelete\", groupPermission.Value.HasFlag(PermissionFlag.Delete));\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n\n                if (includeUsersAndGroups)\n                {\n                    List<User> users = new List<User>(_dnsWebService._authManager.Users);\n                    users.Sort();\n\n                    List<Group> groups = new List<Group>(_dnsWebService._authManager.Groups);\n                    groups.Sort();\n\n                    jsonWriter.WritePropertyName(\"users\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (User user in users)\n                        jsonWriter.WriteStringValue(user.Username);\n\n                    jsonWriter.WriteEndArray();\n\n                    jsonWriter.WritePropertyName(\"groups\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (Group group in groups)\n                        jsonWriter.WriteStringValue(group.Name);\n\n                    jsonWriter.WriteEndArray();\n                }\n            }\n\n            #endregion\n\n            #region public\n\n            public async Task LoginAsync(HttpContext context, UserSessionType sessionType)\n            {\n                HttpRequest request = context.Request;\n\n                string username = request.GetQueryOrForm(\"user\");\n                string password = request.GetQueryOrForm(\"pass\");\n                string totp = request.GetQueryOrForm(\"totp\", null);\n                string tokenName = (sessionType == UserSessionType.ApiToken) ? request.GetQueryOrForm(\"tokenName\") : null;\n                bool includeInfo = request.GetQueryOrForm(\"includeInfo\", bool.Parse, false);\n                IPEndPoint remoteEP = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader);\n\n                UserSession session = await _dnsWebService._authManager.CreateSessionAsync(sessionType, tokenName, username, password, totp, remoteEP.Address, request.Headers.UserAgent);\n\n                _dnsWebService._log.Write(remoteEP, \"[\" + session.User.Username + \"] User logged in.\");\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                if (sessionType == UserSessionType.ApiToken)\n                {\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                        _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n                }\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteCurrentSessionDetails(jsonWriter, session, includeInfo);\n            }\n\n            public void Logout(HttpContext context)\n            {\n                string token = context.Request.GetQueryOrForm(\"token\");\n\n                UserSession session = _dnsWebService._authManager.DeleteSession(token);\n                if (session is not null)\n                {\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + session.User.Username + \"] User logged out.\");\n\n                    _dnsWebService._authManager.SaveConfigFile();\n                }\n            }\n\n            public void GetCurrentSessionDetails(HttpContext context)\n            {\n                UserSession session = context.GetCurrentSession();\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                WriteCurrentSessionDetails(jsonWriter, session, true);\n            }\n\n            public async Task ChangePasswordAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context, true);\n                HttpRequest request = context.Request;\n\n                string password = request.GetQueryOrForm(\"pass\");\n                string totp = request.GetQueryOrForm(\"totp\", null);\n                IPEndPoint remoteEP = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader);\n                string newPassword = request.GetQueryOrForm(\"newPass\");\n                int iterations = request.GetQueryOrForm(\"iterations\", int.Parse, User.DEFAULT_ITERATIONS);\n\n                sessionUser = await _dnsWebService._authManager.ChangePasswordAsync(sessionUser.Username, password, totp, remoteEP.Address, newPassword, iterations);\n\n                _dnsWebService._log.Write(remoteEP, \"[\" + sessionUser.Username + \"] Password was changed successfully.\");\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void Initialize2FA(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context, true);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                if (sessionUser.TOTPEnabled)\n                {\n                    jsonWriter.WriteBoolean(\"totpEnabled\", true);\n                }\n                else\n                {\n                    AuthenticatorKeyUri totpKeyUri = sessionUser.InitializedTOTP(_dnsWebService._dnsServer.ServerDomain);\n\n                    jsonWriter.WriteBoolean(\"totpEnabled\", false);\n                    jsonWriter.WriteString(\"qrCodePngImage\", Convert.ToBase64String(totpKeyUri.GetQRCodePngImage(3)));\n                    jsonWriter.WriteString(\"secret\", totpKeyUri.Secret);\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Two-factor Authentication (2FA) using Time-based one-time password (TOTP) was initialized successfully.\");\n\n                    _dnsWebService._authManager.SaveConfigFile();\n                }\n            }\n\n            public void Enable2FA(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context, true);\n                HttpRequest request = context.Request;\n\n                string totp = request.GetQueryOrForm(\"totp\");\n\n                sessionUser.EnableTOTP(totp);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Two-factor Authentication (2FA) using Time-based one-time password (TOTP) was enabled successfully.\");\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void Disable2FA(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context, true);\n\n                sessionUser.DisableTOTP();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Two-factor Authentication (2FA) using Time-based one-time password (TOTP) was disabled successfully.\");\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void GetProfile(HttpContext context)\n            {\n                UserSession session = context.GetCurrentSession();\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                WriteUserDetails(jsonWriter, session.User, session, true, false);\n            }\n\n            public void SetProfile(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context, true);\n                HttpRequest request = context.Request;\n\n                if (request.TryGetQueryOrForm(\"displayName\", out string displayName))\n                    sessionUser.DisplayName = displayName;\n\n                if (request.TryGetQueryOrForm(\"sessionTimeoutSeconds\", int.Parse, out int sessionTimeoutSeconds))\n                    sessionUser.SessionTimeoutSeconds = sessionTimeoutSeconds;\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] User profile was updated successfully.\");\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                UserSession session = context.GetCurrentSession();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteUserDetails(jsonWriter, sessionUser, session, true, false);\n            }\n\n            public void ListSessions(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"sessions\");\n                jsonWriter.WriteStartArray();\n\n                List<UserSession> sessions = new List<UserSession>(_dnsWebService._authManager.Sessions);\n                sessions.Sort();\n\n                UserSession session = context.GetCurrentSession();\n\n                foreach (UserSession activeSession in sessions)\n                {\n                    if (!activeSession.HasExpired())\n                        WriteUserSessionDetails(jsonWriter, activeSession, session);\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void CreateApiToken(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string username = request.GetQueryOrForm(\"user\");\n                string tokenName = request.GetQueryOrForm(\"tokenName\");\n\n                IPEndPoint remoteEP = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader);\n\n                UserSession createdSession = _dnsWebService._authManager.CreateApiToken(tokenName, username, remoteEP.Address, request.Headers.UserAgent);\n\n                _dnsWebService._log.Write(remoteEP, \"[\" + sessionUser.Username + \"] API token [\" + tokenName + \"] was created successfully for user: \" + username);\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"username\", createdSession.User.Username);\n                jsonWriter.WriteString(\"tokenName\", createdSession.TokenName);\n                jsonWriter.WriteString(\"token\", createdSession.Token);\n            }\n\n            public void DeleteSession(HttpContext context, bool isAdminContext)\n            {\n                UserSession session = context.GetCurrentSession();\n\n                if (isAdminContext)\n                {\n                    if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete))\n                        throw new DnsWebServiceException(\"Access was denied.\");\n                }\n\n                string strPartialToken = context.Request.GetQueryOrForm(\"partialToken\");\n                if (session.Token.StartsWith(strPartialToken))\n                    throw new InvalidOperationException(\"Invalid operation: cannot delete current session.\");\n\n                UserSession sessionToDelete = null;\n\n                foreach (UserSession activeSession in _dnsWebService._authManager.Sessions)\n                {\n                    if (activeSession.Token.StartsWith(strPartialToken))\n                    {\n                        sessionToDelete = activeSession;\n                        break;\n                    }\n                }\n\n                if (sessionToDelete is null)\n                    throw new DnsWebServiceException(\"No such active session was found for partial token: \" + strPartialToken);\n\n                if (!isAdminContext)\n                {\n                    if (sessionToDelete.User != session.User)\n                        throw new DnsWebServiceException(\"Access was denied.\");\n                }\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    if (sessionToDelete.Type == UserSessionType.ApiToken)\n                    {\n                        if (sessionToDelete.TokenName.Equals(_dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))\n                            throw new DnsWebServiceException(\"Invalid operation: cannot delete the Cluster API token.\");\n\n                        if (_dnsWebService._clusterManager.GetSelfNode().Type != Cluster.ClusterNodeType.Primary)\n                            throw new DnsWebServiceException(\"API tokens can be deleted only on the Primary node.\");\n                    }\n                }\n\n                UserSession deletedSession = _dnsWebService._authManager.DeleteSession(sessionToDelete.Token);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + session.User.Username + \"] User session [\" + strPartialToken + \"] was deleted successfully for user: \" + deletedSession.User.Username);\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void ListUsers(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                List<User> users = new List<User>(_dnsWebService._authManager.Users);\n                users.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"users\");\n                jsonWriter.WriteStartArray();\n\n                foreach (User user in users)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    WriteUserDetails(jsonWriter, user, null, false, false);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void CreateUser(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string username = request.GetQueryOrForm(\"user\");\n                string displayName = request.GetQueryOrForm(\"displayName\", username);\n                string password = request.GetQueryOrForm(\"pass\");\n\n                User user = _dnsWebService._authManager.CreateUser(displayName, username, password);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] User account was created successfully with username: \" + user.Username);\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteUserDetails(jsonWriter, user, null, false, false);\n            }\n\n            public void GetUserDetails(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string username = request.GetQueryOrForm(\"user\");\n                bool includeGroups = request.GetQueryOrForm(\"includeGroups\", bool.Parse, false);\n\n                User user = _dnsWebService._authManager.GetUser(username);\n                if (user is null)\n                    throw new DnsWebServiceException(\"No such user exists: \" + username);\n\n                UserSession session = context.GetCurrentSession();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteUserDetails(jsonWriter, user, session, true, includeGroups);\n            }\n\n            public void SetUserDetails(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string username = request.GetQueryOrForm(\"user\");\n\n                User user = _dnsWebService._authManager.GetUser(username);\n                if (user is null)\n                    throw new DnsWebServiceException(\"No such user exists: \" + username);\n\n                try\n                {\n                    if (request.TryGetQueryOrForm(\"displayName\", out string displayName))\n                        user.DisplayName = displayName;\n\n                    if (request.TryGetQueryOrForm(\"newUser\", out string newUsername))\n                        _dnsWebService._authManager.ChangeUsername(user, newUsername);\n\n                    if (request.TryGetQueryOrForm(\"totpEnabled\", bool.Parse, out bool totpEnabled))\n                    {\n                        if (totpEnabled)\n                            throw new InvalidOperationException(\"Time-based one-time password (TOTP) can be enabled only by the user themself.\");\n\n                        user.DisableTOTP();\n                    }\n\n                    if (request.TryGetQueryOrForm(\"disabled\", bool.Parse, out bool disabled) && (sessionUser != user)) //to avoid self lockout\n                    {\n                        user.Disabled = disabled;\n\n                        if (user.Disabled)\n                        {\n                            foreach (UserSession userSession in _dnsWebService._authManager.Sessions)\n                            {\n                                if (userSession.Type == UserSessionType.ApiToken)\n                                    continue;\n\n                                if (userSession.User == user)\n                                    _dnsWebService._authManager.DeleteSession(userSession.Token);\n                            }\n                        }\n                    }\n\n                    if (request.TryGetQueryOrForm(\"sessionTimeoutSeconds\", int.Parse, out int sessionTimeoutSeconds))\n                        user.SessionTimeoutSeconds = sessionTimeoutSeconds;\n\n                    string newPassword = request.QueryOrForm(\"newPass\");\n                    if (!string.IsNullOrWhiteSpace(newPassword))\n                    {\n                        int iterations = request.GetQueryOrForm(\"iterations\", int.Parse, User.DEFAULT_ITERATIONS);\n\n                        user.ChangePassword(newPassword, iterations);\n                    }\n\n                    string memberOfGroups = request.QueryOrForm(\"memberOfGroups\");\n                    if (memberOfGroups is not null)\n                    {\n                        string[] parts = memberOfGroups.Split(',');\n                        Dictionary<string, Group> groups = new Dictionary<string, Group>(parts.Length);\n\n                        foreach (string part in parts)\n                        {\n                            if (part.Length == 0)\n                                continue;\n\n                            Group group = _dnsWebService._authManager.GetGroup(part);\n                            if (group is null)\n                                throw new DnsWebServiceException(\"No such group exists: \" + part);\n\n                            groups.Add(group.Name.ToLowerInvariant(), group);\n                        }\n\n                        //ensure user is member of everyone group\n                        Group everyone = _dnsWebService._authManager.GetGroup(Group.EVERYONE);\n                        groups[everyone.Name.ToLowerInvariant()] = everyone;\n\n                        if (sessionUser == user)\n                        {\n                            //ensure current admin user is member of administrators group to avoid self lockout\n                            Group admins = _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS);\n                            groups[admins.Name.ToLowerInvariant()] = admins;\n                        }\n\n                        user.SyncGroups(groups);\n                    }\n                }\n                finally\n                {\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] User account details were updated successfully for user: \" + username);\n\n                    _dnsWebService._authManager.SaveConfigFile();\n\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                        _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n                }\n\n                UserSession session = context.GetCurrentSession();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteUserDetails(jsonWriter, user, session, true, false);\n            }\n\n            public void DeleteUser(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string username = context.Request.GetQueryOrForm(\"user\");\n\n                if (sessionUser.Username.Equals(username, StringComparison.OrdinalIgnoreCase))\n                    throw new InvalidOperationException(\"Invalid operation: cannot delete current user.\");\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    User userToDelete = _dnsWebService._authManager.GetUser(username);\n                    if (userToDelete is null)\n                        throw new DnsWebServiceException(\"No such user exists: \" + username);\n\n                    List<UserSession> userSessions = _dnsWebService.AuthManager.GetSessions(userToDelete);\n                    bool apiTokenExists = false;\n\n                    foreach (UserSession existingSession in userSessions)\n                    {\n                        if ((existingSession.Type == UserSessionType.ApiToken) && (existingSession.TokenName == _dnsWebService._clusterManager.ClusterDomain))\n                        {\n                            apiTokenExists = true;\n                            break;\n                        }\n                    }\n\n                    if (apiTokenExists)\n                        throw new DnsWebServiceException(\"Invalid operation: cannot delete a user who initialized the Cluster and owns the Cluster API token.\");\n                }\n\n                if (!_dnsWebService._authManager.DeleteUser(username))\n                    throw new DnsWebServiceException(\"Failed to delete user: \" + username);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] User account was deleted successfully with username: \" + username);\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void ListGroups(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                List<Group> groups = new List<Group>(_dnsWebService._authManager.Groups);\n                groups.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"groups\");\n                jsonWriter.WriteStartArray();\n\n                foreach (Group group in groups)\n                {\n                    if (group.Name.Equals(\"Everyone\", StringComparison.OrdinalIgnoreCase))\n                        continue;\n\n                    jsonWriter.WriteStartObject();\n\n                    WriteGroupDetails(jsonWriter, group, false, false);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void CreateGroup(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string groupName = request.GetQueryOrForm(\"group\");\n                string description = request.GetQueryOrForm(\"description\", \"\");\n\n                Group group = _dnsWebService._authManager.CreateGroup(groupName, description);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Group was created successfully with name: \" + group.Name);\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteGroupDetails(jsonWriter, group, false, false);\n            }\n\n            public void GetGroupDetails(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string groupName = request.GetQueryOrForm(\"group\");\n                bool includeUsers = request.GetQueryOrForm(\"includeUsers\", bool.Parse, false);\n\n                Group group = _dnsWebService._authManager.GetGroup(groupName);\n                if (group is null)\n                    throw new DnsWebServiceException(\"No such group exists: \" + groupName);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteGroupDetails(jsonWriter, group, true, includeUsers);\n            }\n\n            public void SetGroupDetails(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string groupName = request.GetQueryOrForm(\"group\");\n\n                Group group = _dnsWebService._authManager.GetGroup(groupName);\n                if (group is null)\n                    throw new DnsWebServiceException(\"No such group exists: \" + groupName);\n\n                if (request.TryGetQueryOrForm(\"newGroup\", out string newGroup))\n                    _dnsWebService._authManager.RenameGroup(group, newGroup);\n\n                if (request.TryGetQueryOrForm(\"description\", out string description))\n                    group.Description = description;\n\n                string members = request.QueryOrForm(\"members\");\n                if (members is not null)\n                {\n                    string[] parts = members.Split(',');\n                    Dictionary<string, User> users = new Dictionary<string, User>();\n\n                    foreach (string part in parts)\n                    {\n                        if (part.Length == 0)\n                            continue;\n\n                        User user = _dnsWebService._authManager.GetUser(part);\n                        if (user is null)\n                            throw new DnsWebServiceException(\"No such user exists: \" + part);\n\n                        users.Add(user.Username, user);\n                    }\n\n                    if (group.Name.Equals(\"administrators\", StringComparison.OrdinalIgnoreCase))\n                        users[sessionUser.Username] = sessionUser; //ensure current admin user is member of administrators group to avoid self lockout\n\n                    _dnsWebService._authManager.SyncGroupMembers(group, users);\n                }\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Group details were updated successfully for group: \" + groupName);\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteGroupDetails(jsonWriter, group, true, false);\n            }\n\n            public void DeleteGroup(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string groupName = context.Request.GetQueryOrForm(\"group\");\n\n                if (!_dnsWebService._authManager.DeleteGroup(groupName))\n                    throw new DnsWebServiceException(\"Failed to delete group: \" + groupName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Group was deleted successfully with name: \" + groupName);\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void ListPermissions(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                List<Permission> permissions = new List<Permission>(_dnsWebService._authManager.Permissions);\n                permissions.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"permissions\");\n                jsonWriter.WriteStartArray();\n\n                foreach (Permission permission in permissions)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    WritePermissionDetails(jsonWriter, permission, null, false);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void GetPermissionDetails(HttpContext context, PermissionSection section)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n                HttpRequest request = context.Request;\n                string strSubItem = null;\n\n                switch (section)\n                {\n                    case PermissionSection.Unknown:\n                        if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                            throw new DnsWebServiceException(\"Access was denied.\");\n\n                        section = request.GetQueryOrFormEnum<PermissionSection>(\"section\");\n                        break;\n\n                    case PermissionSection.Zones:\n                        if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                            throw new DnsWebServiceException(\"Access was denied.\");\n\n                        strSubItem = request.GetQueryOrForm(\"zone\").Trim('.');\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n\n                bool includeUsersAndGroups = request.GetQueryOrForm(\"includeUsersAndGroups\", bool.Parse, false);\n\n                if (strSubItem is not null)\n                {\n                    if (!_dnsWebService._authManager.IsPermitted(section, strSubItem, sessionUser, PermissionFlag.View))\n                        throw new DnsWebServiceException(\"Access was denied.\");\n                }\n\n                Permission permission;\n\n                if (strSubItem is null)\n                    permission = _dnsWebService._authManager.GetPermission(section);\n                else\n                    permission = _dnsWebService._authManager.GetPermission(section, strSubItem);\n\n                if (permission is null)\n                    throw new DnsWebServiceException(\"No permissions exists for section: \" + section.ToString() + (strSubItem is null ? \"\" : \"/\" + strSubItem));\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WritePermissionDetails(jsonWriter, permission, strSubItem, includeUsersAndGroups);\n            }\n\n            public void SetPermissionsDetails(HttpContext context, PermissionSection section)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n                HttpRequest request = context.Request;\n                string strSubItem = null;\n\n                switch (section)\n                {\n                    case PermissionSection.Unknown:\n                        if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                            throw new DnsWebServiceException(\"Access was denied.\");\n\n                        if (_dnsWebService._clusterManager.ClusterInitialized)\n                        {\n                            if (_dnsWebService._clusterManager.GetSelfNode().Type != Cluster.ClusterNodeType.Primary)\n                                throw new DnsWebServiceException(\"Permissions for sections can be set only on the Primary node.\");\n                        }\n\n                        section = request.GetQueryOrFormEnum<PermissionSection>(\"section\");\n                        break;\n\n                    case PermissionSection.Zones:\n                        if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                            throw new DnsWebServiceException(\"Access was denied.\");\n\n                        strSubItem = request.GetQueryOrForm(\"zone\").Trim('.');\n                        break;\n\n                    default:\n                        throw new InvalidOperationException();\n                }\n\n                if (strSubItem is not null)\n                {\n                    if (!_dnsWebService._authManager.IsPermitted(section, strSubItem, sessionUser, PermissionFlag.Delete))\n                        throw new DnsWebServiceException(\"Access was denied.\");\n                }\n\n                Permission permission;\n\n                if (strSubItem is null)\n                    permission = _dnsWebService._authManager.GetPermission(section);\n                else\n                    permission = _dnsWebService._authManager.GetPermission(section, strSubItem);\n\n                if (permission is null)\n                    throw new DnsWebServiceException(\"No permissions exists for section: \" + section.ToString() + (strSubItem is null ? \"\" : \"/\" + strSubItem));\n\n                string strUserPermissions = request.QueryOrForm(\"userPermissions\");\n                if (strUserPermissions is not null)\n                {\n                    string[] parts = strUserPermissions.Split('|');\n                    Dictionary<User, PermissionFlag> userPermissions = new Dictionary<User, PermissionFlag>();\n\n                    for (int i = 0; i < parts.Length; i += 4)\n                    {\n                        if (parts[i].Length == 0)\n                            continue;\n\n                        User user = _dnsWebService._authManager.GetUser(parts[i]);\n                        bool canView = bool.Parse(parts[i + 1]);\n                        bool canModify = bool.Parse(parts[i + 2]);\n                        bool canDelete = bool.Parse(parts[i + 3]);\n\n                        if (user is not null)\n                        {\n                            PermissionFlag permissionFlag = PermissionFlag.None;\n\n                            if (canView)\n                                permissionFlag |= PermissionFlag.View;\n\n                            if (canModify)\n                                permissionFlag |= PermissionFlag.Modify;\n\n                            if (canDelete)\n                                permissionFlag |= PermissionFlag.Delete;\n\n                            userPermissions[user] = permissionFlag;\n                        }\n                    }\n\n                    permission.SyncPermissions(userPermissions);\n                }\n\n                string strGroupPermissions = request.QueryOrForm(\"groupPermissions\");\n                if (strGroupPermissions is not null)\n                {\n                    string[] parts = strGroupPermissions.Split('|');\n                    Dictionary<Group, PermissionFlag> groupPermissions = new Dictionary<Group, PermissionFlag>();\n\n                    for (int i = 0; i < parts.Length; i += 4)\n                    {\n                        if (parts[i].Length == 0)\n                            continue;\n\n                        Group group = _dnsWebService._authManager.GetGroup(parts[i]);\n                        bool canView = bool.Parse(parts[i + 1]);\n                        bool canModify = bool.Parse(parts[i + 2]);\n                        bool canDelete = bool.Parse(parts[i + 3]);\n\n                        if (group is not null)\n                        {\n                            PermissionFlag permissionFlag = PermissionFlag.None;\n\n                            if (canView)\n                                permissionFlag |= PermissionFlag.View;\n\n                            if (canModify)\n                                permissionFlag |= PermissionFlag.Modify;\n\n                            if (canDelete)\n                                permissionFlag |= PermissionFlag.Delete;\n\n                            groupPermissions[group] = permissionFlag;\n                        }\n                    }\n\n                    //ensure administrators group always has all permissions\n                    Group admins = _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS);\n                    groupPermissions[admins] = PermissionFlag.ViewModifyDelete;\n\n                    switch (section)\n                    {\n                        case PermissionSection.Zones:\n                            //ensure DNS administrators group always has all permissions\n                            Group dnsAdmins = _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS);\n                            groupPermissions[dnsAdmins] = PermissionFlag.ViewModifyDelete;\n                            break;\n\n                        case PermissionSection.DhcpServer:\n                            //ensure DHCP administrators group always has all permissions\n                            Group dhcpAdmins = _dnsWebService._authManager.GetGroup(Group.DHCP_ADMINISTRATORS);\n                            groupPermissions[dhcpAdmins] = PermissionFlag.ViewModifyDelete;\n                            break;\n                    }\n\n                    permission.SyncPermissions(groupPermissions);\n                }\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Permissions were updated successfully for section: \" + section.ToString() + (string.IsNullOrEmpty(strSubItem) ? \"\" : \"/\" + strSubItem));\n\n                _dnsWebService._authManager.SaveConfigFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WritePermissionDetails(jsonWriter, permission, strSubItem, false);\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceClusterApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Cluster;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Buffers.Text;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Net;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        sealed class WebServiceClusterApi\n        {\n            #region variables\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceClusterApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region private\n\n            private void WriteClusterState(Utf8JsonWriter jsonWriter, bool includeServerIpAddresses = false)\n            {\n                jsonWriter.WriteString(\"version\", _dnsWebService.GetServerVersion());\n                jsonWriter.WriteString(\"dnsServerDomain\", _dnsWebService._dnsServer.ServerDomain);\n                jsonWriter.WriteBoolean(\"clusterInitialized\", _dnsWebService._clusterManager.ClusterInitialized);\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    jsonWriter.WriteString(\"clusterDomain\", _dnsWebService._clusterManager.ClusterDomain);\n\n                    jsonWriter.WriteNumber(\"heartbeatRefreshIntervalSeconds\", _dnsWebService._clusterManager.HeartbeatRefreshIntervalSeconds);\n                    jsonWriter.WriteNumber(\"heartbeatRetryIntervalSeconds\", _dnsWebService._clusterManager.HeartBeatRetryIntervalSeconds);\n                    jsonWriter.WriteNumber(\"configRefreshIntervalSeconds\", _dnsWebService._clusterManager.ConfigRefreshIntervalSeconds);\n                    jsonWriter.WriteNumber(\"configRetryIntervalSeconds\", _dnsWebService._clusterManager.ConfigRetryIntervalSeconds);\n\n                    WriteClusterNodes(jsonWriter);\n                }\n\n                if (includeServerIpAddresses)\n                {\n                    jsonWriter.WriteStartArray(\"serverIpAddresses\");\n\n                    foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces())\n                    {\n                        if (networkInterface.OperationalStatus != OperationalStatus.Up)\n                            continue;\n\n                        foreach (UnicastIPAddressInformation ip in networkInterface.GetIPProperties().UnicastAddresses)\n                        {\n                            if (IPAddress.IsLoopback(ip.Address))\n                                continue;\n\n                            switch (ip.Address.AddressFamily)\n                            {\n                                case AddressFamily.InterNetwork:\n                                    jsonWriter.WriteStringValue(ip.Address.ToString());\n                                    break;\n\n                                case AddressFamily.InterNetworkV6:\n                                    if (ip.Address.IsIPv6LinkLocal || ip.Address.IsIPv6Teredo)\n                                        continue;\n\n                                    jsonWriter.WriteStringValue(ip.Address.ToString());\n                                    break;\n                            }\n                        }\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n            }\n\n            internal void WriteClusterNodes(Utf8JsonWriter jsonWriter)\n            {\n                List<ClusterNode> sortedClusterNodes = [.. _dnsWebService._clusterManager.ClusterNodes.Values];\n                sortedClusterNodes.Sort();\n\n                jsonWriter.WriteStartArray(\"clusterNodes\");\n\n                foreach (ClusterNode clusterNode in sortedClusterNodes)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteNumber(\"id\", clusterNode.Id);\n                    jsonWriter.WriteString(\"name\", clusterNode.Name);\n                    jsonWriter.WriteString(\"url\", clusterNode.Url.OriginalString);\n\n                    jsonWriter.WriteStartArray(\"ipAddresses\");\n\n                    foreach (IPAddress ipAddress in clusterNode.IPAddresses)\n                        jsonWriter.WriteStringValue(ipAddress.ToString());\n\n                    jsonWriter.WriteEndArray();\n\n                    jsonWriter.WriteString(\"type\", clusterNode.Type.ToString());\n                    jsonWriter.WriteString(\"state\", clusterNode.State.ToString());\n\n                    if (clusterNode.State == ClusterNodeState.Self)\n                    {\n                        jsonWriter.WriteString(\"upSince\", clusterNode.UpSince);\n\n                        if (clusterNode.Type == ClusterNodeType.Secondary)\n                        {\n                            if (_dnsWebService._clusterManager.ConfigLastSynced != default)\n                                jsonWriter.WriteString(\"configLastSynced\", _dnsWebService._clusterManager.ConfigLastSynced);\n                        }\n                    }\n                    else\n                    {\n                        if (clusterNode.UpSince != default)\n                            jsonWriter.WriteString(\"upSince\", clusterNode.UpSince);\n\n                        if (clusterNode.LastSeen != default)\n                            jsonWriter.WriteString(\"lastSeen\", clusterNode.LastSeen);\n                    }\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            private void EnableWebServiceTlsWithSelfSignedCertificate()\n            {\n                _dnsWebService._webServiceEnableTls = true;\n                _dnsWebService._webServiceUseSelfSignedTlsCertificate = true;\n                _dnsWebService._webServiceTlsCertificatePath = null;\n                _dnsWebService._webServiceTlsCertificatePassword = null;\n\n                _dnsWebService.CheckAndLoadSelfSignedCertificate(false, true);\n\n                _dnsWebService.SaveConfigFile();\n            }\n\n            private void RestartWebService()\n            {\n                ThreadPool.QueueUserWorkItem(async delegate (object state)\n                {\n                    try\n                    {\n                        await Task.Delay(2000); //wait for the current HTTP response to be delivered before restarting web server\n\n                        _dnsWebService._log.Write(\"Attempting to restart web service.\");\n\n                        await _dnsWebService.StopWebServiceAsync();\n                        await _dnsWebService.StartWebServiceAsync(false);\n\n                        _dnsWebService._log.Write(\"Web service was restarted successfully.\");\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsWebService._log.Write(\"Failed to restart web service.\\r\\n\" + ex.ToString());\n                        _dnsWebService._log.Write(\"Attempting to restart web service in HTTP only mode.\");\n\n                        try\n                        {\n                            await _dnsWebService.StopWebServiceAsync();\n                            await _dnsWebService.StartWebServiceAsync(true);\n                        }\n                        catch (Exception ex2)\n                        {\n                            _dnsWebService._log.Write(\"Failed to restart web service in HTTP only mode.\\r\\n\" + ex2.ToString());\n                        }\n                    }\n                });\n            }\n\n            #endregion\n\n            #region public\n\n            public void GetClusterState(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                bool includeServerIpAddresses = request.GetQueryOrForm(\"includeServerIpAddresses\", bool.Parse, false);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter, includeServerIpAddresses);\n            }\n\n            public void InitializeCluster(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string clusterDomain = request.GetQueryOrForm(\"clusterDomain\").TrimEnd('.');\n\n                if (!request.TryGetQueryOrFormArray(\"primaryNodeIpAddresses\", IPAddress.Parse, out IPAddress[] primaryNodeIpAddresses))\n                    throw new DnsWebServiceException(\"Parameter 'primaryNodeIpAddresses' missing.\");\n\n                bool restartWebService = false;\n\n                //enable TLS web service if not already enabled\n                if (!_dnsWebService.IsWebServiceTlsEnabled)\n                {\n                    EnableWebServiceTlsWithSelfSignedCertificate();\n                    restartWebService = true;\n                }\n\n                try\n                {\n                    _dnsWebService._clusterManager.InitializeCluster(clusterDomain, primaryNodeIpAddresses, context.GetCurrentSession());\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Cluster (\" + _dnsWebService._clusterManager.ClusterDomain + \") was initialized successfully.\");\n\n                    Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                    WriteClusterState(jsonWriter);\n                }\n                finally\n                {\n                    //restart TLS web service to apply HTTPS changes\n                    if (restartWebService)\n                        RestartWebService();\n                }\n            }\n\n            public void DeleteCluster(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                bool forceDelete = request.GetQueryOrForm(\"forceDelete\", bool.Parse, false);\n\n                string clusterDomain = _dnsWebService._clusterManager.ClusterDomain;\n                _dnsWebService._clusterManager.DeleteCluster(forceDelete);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Cluster (\" + clusterDomain + \") was deleted successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public void JoinCluster(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                int secondaryNodeId = request.GetQueryOrForm(\"secondaryNodeId\", int.Parse);\n                Uri secondaryNodeUrl = new Uri(request.GetQueryOrForm(\"secondaryNodeUrl\"));\n\n                if (!request.TryGetQueryOrFormArray(\"secondaryNodeIpAddresses\", IPAddress.Parse, out IPAddress[] secondaryNodeIpAddresses))\n                    throw new DnsWebServiceException(\"Parameter 'secondaryNodeIpAddresses' missing.\");\n\n                X509Certificate2 secondaryNodeCertificate = X509CertificateLoader.LoadCertificate(Base64Url.DecodeFromChars(request.GetQueryOrForm(\"secondaryNodeCertificate\")));\n\n                ClusterNode secondaryNode = _dnsWebService._clusterManager.JoinCluster(secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, secondaryNodeCertificate);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Secondary node '\" + secondaryNode.ToString() + \"' joined the Cluster (\" + _dnsWebService._clusterManager.ClusterDomain + \") successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public async Task RemoveSecondaryNodeAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                int secondaryNodeId = request.GetQueryOrForm(\"secondaryNodeId\", int.Parse);\n\n                ClusterNode secondaryNode = await _dnsWebService._clusterManager.AskSecondaryNodeToLeaveClusterAsync(secondaryNodeId);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Secondary node '\" + secondaryNode.ToString() + \"' was asked to leave the Cluster (\" + _dnsWebService._clusterManager.ClusterDomain + \") successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public void DeleteSecondaryNode(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                int secondaryNodeId = request.GetQueryOrForm(\"secondaryNodeId\", int.Parse);\n\n                ClusterNode secondaryNode = _dnsWebService._clusterManager.DeleteSecondaryNode(secondaryNodeId);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Secondary node '\" + secondaryNode.ToString() + \"' was deleted from the Cluster (\" + _dnsWebService._clusterManager.ClusterDomain + \") successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public void UpdateSecondaryNode(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                int secondaryNodeId = request.GetQueryOrForm(\"secondaryNodeId\", int.Parse);\n                Uri secondaryNodeUrl = new Uri(request.GetQueryOrForm(\"secondaryNodeUrl\"));\n\n                if (!request.TryGetQueryOrFormArray(\"secondaryNodeIpAddresses\", IPAddress.Parse, out IPAddress[] secondaryNodeIpAddresses))\n                    throw new DnsWebServiceException(\"Parameter 'secondaryNodeIpAddresses' missing.\");\n\n                X509Certificate2 secondaryNodeCertificate = X509CertificateLoader.LoadCertificate(Base64Url.DecodeFromChars(request.GetQueryOrForm(\"secondaryNodeCertificate\")));\n\n                ClusterNode secondaryNode = _dnsWebService._clusterManager.UpdateSecondaryNode(secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, secondaryNodeCertificate);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Secondary node '\" + secondaryNode.ToString() + \"' details were updated successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public async Task TransferConfigAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string ifModifiedSinceValue = request.Headers.IfModifiedSince;\n                string includeZonesValue = request.QueryOrForm(\"includeZones\");\n\n                DateTime ifModifiedSince = string.IsNullOrEmpty(ifModifiedSinceValue) ? DateTime.UnixEpoch : DateTime.ParseExact(ifModifiedSinceValue, \"R\", CultureInfo.InvariantCulture);\n                string[] includeZones = string.IsNullOrEmpty(includeZonesValue) ? null : includeZonesValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n\n                string tmpFile = Path.GetTempFileName();\n                try\n                {\n                    await using (FileStream configZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                    {\n                        //create config zip file\n                        await _dnsWebService._clusterManager.TransferConfigAsync(configZipStream, ifModifiedSince, includeZones);\n\n                        //send config zip file\n                        configZipStream.Position = 0;\n\n                        HttpResponse response = context.Response;\n\n                        response.ContentType = \"application/zip\";\n                        response.ContentLength = configZipStream.Length;\n                        response.Headers.LastModified = DateTime.UtcNow.ToString(\"R\");\n                        response.Headers.Append(\"Content-Disposition\", \"attachment; filename=\\\"config.zip\\\"\");\n\n                        await using (Stream output = response.Body)\n                        {\n                            await configZipStream.CopyToAsync(output);\n                        }\n                    }\n                }\n                finally\n                {\n                    try\n                    {\n                        File.Delete(tmpFile);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsWebService._log.Write(ex);\n                    }\n                }\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Server configuration was transferred successfully.\");\n            }\n\n            public void SetClusterOptions(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                ushort heartbeatRefreshIntervalSeconds = request.GetQueryOrForm(\"heartbeatRefreshIntervalSeconds\", ushort.Parse, _dnsWebService._clusterManager.HeartbeatRefreshIntervalSeconds);\n                ushort heartbeatRetryIntervalSeconds = request.GetQueryOrForm(\"heartbeatRetryIntervalSeconds\", ushort.Parse, _dnsWebService._clusterManager.HeartBeatRetryIntervalSeconds);\n                ushort configRefreshIntervalSeconds = request.GetQueryOrForm(\"configRefreshIntervalSeconds\", ushort.Parse, _dnsWebService._clusterManager.ConfigRefreshIntervalSeconds);\n                ushort configRetryIntervalSeconds = request.GetQueryOrForm(\"configRetryIntervalSeconds\", ushort.Parse, _dnsWebService._clusterManager.ConfigRetryIntervalSeconds);\n\n                _dnsWebService._clusterManager.UpdateClusterOptions(heartbeatRefreshIntervalSeconds, heartbeatRetryIntervalSeconds, configRefreshIntervalSeconds, configRetryIntervalSeconds);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Cluster (\" + _dnsWebService._clusterManager.ClusterDomain + \") options were updated successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public async Task InitializeAndJoinClusterAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                if (!request.TryGetQueryOrFormArray(\"secondaryNodeIpAddresses\", IPAddress.Parse, out IPAddress[] secondaryNodeIpAddresses))\n                    throw new DnsWebServiceException(\"Parameter 'secondaryNodeIpAddresses' missing.\");\n\n                Uri primaryNodeUrl = new Uri(request.GetQueryOrForm(\"primaryNodeUrl\"));\n                IPAddress primaryNodeIpAddress = request.GetQueryOrForm(\"primaryNodeIpAddress\", IPAddress.Parse, null);\n                string primaryNodeUsername = request.GetQueryOrForm(\"primaryNodeUsername\");\n                string primaryNodePassword = request.GetQueryOrForm(\"primaryNodePassword\");\n                string primaryNodeTotp = request.GetQueryOrForm(\"primaryNodeTotp\", null);\n                bool ignoreCertificateErrors = request.GetQueryOrForm(\"ignoreCertificateErrors\", bool.Parse, false);\n\n                bool restartWebService = false;\n\n                //enable TLS web service if not already enabled\n                if (!_dnsWebService.IsWebServiceTlsEnabled)\n                {\n                    EnableWebServiceTlsWithSelfSignedCertificate();\n                    restartWebService = true;\n                }\n\n                try\n                {\n                    await _dnsWebService._clusterManager.InitializeAndJoinClusterAsync(secondaryNodeIpAddresses, primaryNodeUrl, primaryNodeUsername, primaryNodePassword, primaryNodeTotp, primaryNodeIpAddress is null ? null : [primaryNodeIpAddress], ignoreCertificateErrors);\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Joined the Cluster (\" + _dnsWebService._clusterManager.ClusterDomain + \") as a Secondary node successfully.\");\n\n                    Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                    WriteClusterState(jsonWriter);\n                }\n                finally\n                {\n                    //restart TLS web service to apply HTTPS changes\n                    if (restartWebService)\n                        RestartWebService();\n                }\n            }\n\n            public async Task LeaveClusterAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                bool forceLeave = request.GetQueryOrForm(\"forceLeave\", bool.Parse, false);\n\n                string clusterDomain = _dnsWebService._clusterManager.ClusterDomain;\n                await _dnsWebService._clusterManager.LeaveClusterAsync(forceLeave);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Left the Cluster (\" + clusterDomain + \") successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public async Task ConfigUpdateNotificationAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                int primaryNodeId = request.GetQueryOrForm(\"primaryNodeId\", int.Parse);\n                Uri primaryNodeUrl = new Uri(request.GetQueryOrForm(\"primaryNodeUrl\"));\n\n                if (!request.TryGetQueryOrFormArray(\"primaryNodeIpAddresses\", IPAddress.Parse, out IPAddress[] primaryNodeIpAddresses))\n                    throw new DnsWebServiceException(\"Parameter 'primaryNodeIpAddresses' missing.\");\n\n                //update primary node\n                ClusterNode primaryNode = await _dnsWebService._clusterManager.UpdatePrimaryNodeAsync(primaryNodeUrl, primaryNodeIpAddresses, primaryNodeId);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Notification for configuration update was received. Primary node '\" + primaryNode.ToString() + \"' details were updated successfully.\");\n            }\n\n            public void ResyncCluster(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._clusterManager.TriggerResyncForConfig();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Resync for configuration and Cluster Secondary zones was triggered successfully.\");\n            }\n\n            public async Task UpdatePrimaryNodeAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                Uri primaryNodeUrl = new Uri(request.GetQueryOrForm(\"primaryNodeUrl\"));\n\n                if (!request.TryGetQueryOrFormArray(\"primaryNodeIpAddresses\", IPAddress.Parse, out IPAddress[] primaryNodeIpAddresses))\n                    primaryNodeIpAddresses = null;\n\n                //update primary node\n                ClusterNode primaryNode = await _dnsWebService._clusterManager.UpdatePrimaryNodeAsync(primaryNodeUrl, primaryNodeIpAddresses);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary node '\" + primaryNode.ToString() + \"' details were updated successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public async Task PromoteToPrimaryNodeAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                bool forceDeletePrimary = request.GetQueryOrForm(\"forceDeletePrimary\", bool.Parse, false);\n\n                //promote to primary node\n                await _dnsWebService._clusterManager.PromoteToPrimaryNodeAsync(forceDeletePrimary);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] This Secondary node was promoted to be a Primary node for the Cluster successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            public void UpdateSelfNodeIPAddress(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                if (!request.TryGetQueryOrFormArray(\"ipAddresses\", IPAddress.Parse, out IPAddress[] ipAddresses))\n                    throw new DnsWebServiceException(\"Parameter 'ipAddresses' missing.\");\n\n                //update self node IP address\n                ClusterNode selfNode = _dnsWebService._clusterManager.UpdateSelfNodeIPAddresses(ipAddresses);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] \" + selfNode.Type.ToString() + \" node '\" + selfNode.ToString() + \"' IP address was updated successfully.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteClusterState(jsonWriter);\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceDashboardApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.HttpApi.Models;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        class WebServiceDashboardApi\n        {\n            #region variables\n\n            readonly DnsWebService _dnsWebService;\n\n            const int CLUSTER_NODE_DASHBOARD_STATS_API_TIMEOUT = 10000;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceDashboardApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region private\n\n            private static void WriteChartDataSet(Utf8JsonWriter jsonWriter, DashboardStats.DataSet dataSet, string backgroundColor, string borderColor)\n            {\n                jsonWriter.WriteStartObject();\n\n                jsonWriter.WriteString(\"label\", dataSet.Label);\n                jsonWriter.WriteString(\"backgroundColor\", backgroundColor);\n                jsonWriter.WriteString(\"borderColor\", borderColor);\n                jsonWriter.WriteNumber(\"borderWidth\", 2);\n                jsonWriter.WriteBoolean(\"fill\", true);\n\n                jsonWriter.WritePropertyName(\"data\");\n                jsonWriter.WriteStartArray();\n\n                foreach (long value in dataSet.Data)\n                    jsonWriter.WriteNumberValue(value);\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WriteEndObject();\n            }\n\n            private async Task ResolvePtrTopClientsAsync(DashboardStats.TopClientStats[] topClients)\n            {\n                IDictionary<string, string> dhcpClientIpMap = _dnsWebService._dhcpServer.GetAddressHostNameMap();\n\n                async Task ResolvePtrAsync(DashboardStats.TopClientStats item)\n                {\n                    string ip = item.Name;\n\n                    if (dhcpClientIpMap.TryGetValue(ip, out string dhcpDomain))\n                    {\n                        item.Domain = dhcpDomain;\n                        return;\n                    }\n\n                    IPAddress address = IPAddress.Parse(ip);\n\n                    if (IPAddress.IsLoopback(address))\n                    {\n                        item.Domain = \"localhost\";\n                        return;\n                    }\n\n                    DnsDatagram ptrResponse = await _dnsWebService._dnsServer.DirectQueryAsync(new DnsQuestionRecord(address, DnsClass.IN), 500);\n                    if (ptrResponse.Answer.Count > 0)\n                    {\n                        IReadOnlyList<string> ptrDomains = DnsClient.ParseResponsePTR(ptrResponse);\n                        if (ptrDomains.Count > 0)\n                        {\n                            item.Domain = ptrDomains[0];\n                            return;\n                        }\n                    }\n                }\n\n                List<Task> resolverTasks = new List<Task>(topClients.Length);\n\n                foreach (DashboardStats.TopClientStats item in topClients)\n                {\n                    if (string.IsNullOrEmpty(item.Domain))\n                        resolverTasks.Add(ResolvePtrAsync(item));\n                }\n\n                foreach (Task resolverTask in resolverTasks)\n                {\n                    try\n                    {\n                        await resolverTask;\n                    }\n                    catch\n                    { }\n                }\n            }\n\n            #endregion\n\n            #region public\n\n            public async Task GetStats(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                DashboardStatsType type = request.GetQueryOrFormEnum(\"type\", DashboardStatsType.LastHour);\n                bool utcFormat = request.GetQueryOrForm(\"utc\", bool.Parse, false);\n\n                bool isLanguageEnUs = true;\n                string acceptLanguage = request.Headers.AcceptLanguage;\n                if (!string.IsNullOrEmpty(acceptLanguage))\n                    isLanguageEnUs = acceptLanguage.StartsWith(\"en-us\", StringComparison.OrdinalIgnoreCase);\n\n                bool dontTrimQueryTypeData = request.GetQueryOrForm(\"dontTrimQueryTypeData\", bool.Parse, false);\n\n                DateTime startDate = default;\n                DateTime endDate = default;\n\n                if (type == DashboardStatsType.Custom)\n                {\n                    string strStartDate = request.GetQueryOrForm(\"start\");\n                    string strEndDate = request.GetQueryOrForm(\"end\");\n\n                    if (!DateTime.TryParse(strStartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out startDate))\n                        throw new DnsWebServiceException(\"Invalid start date format.\");\n\n                    if (!DateTime.TryParse(strEndDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out endDate))\n                        throw new DnsWebServiceException(\"Invalid end date format.\");\n\n                    if (startDate > endDate)\n                        throw new DnsWebServiceException(\"Start date must be less than or equal to end date.\");\n                }\n\n                List<Task<DashboardStats>> tasks = null;\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    string node = request.GetQueryOrForm(\"node\", null);\n                    if (\"cluster\".Equals(node, StringComparison.OrdinalIgnoreCase))\n                    {\n                        IReadOnlyDictionary<int, Cluster.ClusterNode> clusterNodes = _dnsWebService._clusterManager.ClusterNodes;\n                        tasks = new List<Task<DashboardStats>>(clusterNodes.Count);\n\n                        foreach (KeyValuePair<int, Cluster.ClusterNode> clusterNode in clusterNodes)\n                        {\n                            if (clusterNode.Value.State == Cluster.ClusterNodeState.Self)\n                                continue;\n\n                            tasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                            {\n                                return clusterNode.Value.GetDashboardStatsAsync(sessionUser, type, utcFormat, acceptLanguage, true, startDate, endDate, cancellationToken1);\n                            }, CLUSTER_NODE_DASHBOARD_STATS_API_TIMEOUT));\n                        }\n                    }\n                }\n\n                DashboardStats dashboardStats;\n                string labelFormat;\n\n                switch (type)\n                {\n                    case DashboardStatsType.LastHour:\n                        dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastHourMinuteWiseStats(utcFormat);\n                        labelFormat = \"HH:mm\";\n                        break;\n\n                    case DashboardStatsType.LastDay:\n                        dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastDayHourWiseStats(utcFormat);\n\n                        if (isLanguageEnUs)\n                            labelFormat = \"MM/DD HH:00\";\n                        else\n                            labelFormat = \"DD/MM HH:00\";\n\n                        break;\n\n                    case DashboardStatsType.LastWeek:\n                        dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastWeekDayWiseStats(utcFormat);\n\n                        if (isLanguageEnUs)\n                            labelFormat = \"MM/DD\";\n                        else\n                            labelFormat = \"DD/MM\";\n\n                        break;\n\n                    case DashboardStatsType.LastMonth:\n                        dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastMonthDayWiseStats(utcFormat);\n\n                        if (isLanguageEnUs)\n                            labelFormat = \"MM/DD\";\n                        else\n                            labelFormat = \"DD/MM\";\n\n                        break;\n\n                    case DashboardStatsType.LastYear:\n                        labelFormat = \"MM/YYYY\";\n                        dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastYearMonthWiseStats(utcFormat);\n                        break;\n\n                    case DashboardStatsType.Custom:\n                        TimeSpan duration = endDate - startDate;\n\n                        if ((Convert.ToInt32(duration.TotalDays) + 1) > 7)\n                        {\n                            dashboardStats = _dnsWebService._dnsServer.StatsManager.GetDayWiseStats(startDate, endDate, utcFormat);\n\n                            if (isLanguageEnUs)\n                                labelFormat = \"MM/DD\";\n                            else\n                                labelFormat = \"DD/MM\";\n                        }\n                        else if ((Convert.ToInt32(duration.TotalHours) + 1) > 3)\n                        {\n                            dashboardStats = _dnsWebService._dnsServer.StatsManager.GetHourWiseStats(startDate, endDate, utcFormat);\n\n                            if (isLanguageEnUs)\n                                labelFormat = \"MM/DD HH:00\";\n                            else\n                                labelFormat = \"DD/MM HH:00\";\n                        }\n                        else\n                        {\n                            dashboardStats = _dnsWebService._dnsServer.StatsManager.GetMinuteWiseStats(startDate, endDate, utcFormat);\n\n                            if (isLanguageEnUs)\n                                labelFormat = \"MM/DD HH:mm\";\n                            else\n                                labelFormat = \"DD/MM HH:mm\";\n                        }\n\n                        break;\n\n                    default:\n                        throw new DnsWebServiceException(\"Unknown stats type requested: \" + type.ToString());\n                }\n\n                //add extra stats\n                {\n                    dashboardStats.Stats.Zones = _dnsWebService._dnsServer.AuthZoneManager.TotalZones;\n                    dashboardStats.Stats.CachedEntries = _dnsWebService._dnsServer.CacheZoneManager.TotalEntries;\n                    dashboardStats.Stats.AllowedZones = _dnsWebService._dnsServer.AllowedZoneManager.TotalZonesAllowed;\n                    dashboardStats.Stats.BlockedZones = _dnsWebService._dnsServer.BlockedZoneManager.TotalZonesBlocked;\n                    dashboardStats.Stats.AllowListZones = _dnsWebService._dnsServer.BlockListZoneManager.TotalZonesAllowed;\n                    dashboardStats.Stats.BlockListZones = _dnsWebService._dnsServer.BlockListZoneManager.TotalZonesBlocked;\n                }\n\n                if (tasks is not null)\n                {\n                    foreach (Task<DashboardStats> task in tasks)\n                    {\n                        try\n                        {\n                            dashboardStats.Merge(await task, 10);\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(ex);\n                        }\n                    }\n                }\n\n                if (!dontTrimQueryTypeData)\n                    dashboardStats.QueryTypeChartData.Trim(10); //trim query type data\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                //stats\n                {\n                    jsonWriter.WritePropertyName(\"stats\");\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteNumber(\"totalQueries\", dashboardStats.Stats.TotalQueries);\n                    jsonWriter.WriteNumber(\"totalNoError\", dashboardStats.Stats.TotalNoError);\n                    jsonWriter.WriteNumber(\"totalServerFailure\", dashboardStats.Stats.TotalServerFailure);\n                    jsonWriter.WriteNumber(\"totalNxDomain\", dashboardStats.Stats.TotalNxDomain);\n                    jsonWriter.WriteNumber(\"totalRefused\", dashboardStats.Stats.TotalRefused);\n\n                    jsonWriter.WriteNumber(\"totalAuthoritative\", dashboardStats.Stats.TotalAuthoritative);\n                    jsonWriter.WriteNumber(\"totalRecursive\", dashboardStats.Stats.TotalRecursive);\n                    jsonWriter.WriteNumber(\"totalCached\", dashboardStats.Stats.TotalCached);\n                    jsonWriter.WriteNumber(\"totalBlocked\", dashboardStats.Stats.TotalBlocked);\n                    jsonWriter.WriteNumber(\"totalDropped\", dashboardStats.Stats.TotalDropped);\n\n                    jsonWriter.WriteNumber(\"totalClients\", dashboardStats.Stats.TotalClients);\n\n                    jsonWriter.WriteNumber(\"zones\", dashboardStats.Stats.Zones);\n                    jsonWriter.WriteNumber(\"cachedEntries\", dashboardStats.Stats.CachedEntries);\n                    jsonWriter.WriteNumber(\"allowedZones\", dashboardStats.Stats.AllowedZones);\n                    jsonWriter.WriteNumber(\"blockedZones\", dashboardStats.Stats.BlockedZones);\n                    jsonWriter.WriteNumber(\"allowListZones\", dashboardStats.Stats.AllowListZones);\n                    jsonWriter.WriteNumber(\"blockListZones\", dashboardStats.Stats.BlockListZones);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                //main chart\n                {\n                    jsonWriter.WritePropertyName(\"mainChartData\");\n                    jsonWriter.WriteStartObject();\n\n                    //label format\n                    {\n                        jsonWriter.WriteString(\"labelFormat\", labelFormat);\n                    }\n\n                    //label\n                    {\n                        jsonWriter.WritePropertyName(\"labels\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (string label in dashboardStats.MainChartData.Labels)\n                            jsonWriter.WriteStringValue(label);\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    //datasets\n                    {\n                        jsonWriter.WritePropertyName(\"datasets\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (DashboardStats.DataSet dataSet in dashboardStats.MainChartData.DataSets)\n                        {\n                            string backgroundColor;\n                            string borderColor;\n\n                            switch (dataSet.Label)\n                            {\n                                case \"Total\":\n                                    backgroundColor = \"rgba(102, 153, 255, 0.1)\";\n                                    borderColor = \"rgb(102, 153, 255)\";\n                                    break;\n\n                                case \"No Error\":\n                                    backgroundColor = \"rgba(92, 184, 92, 0.1)\";\n                                    borderColor = \"rgb(92, 184, 92)\";\n                                    break;\n\n                                case \"Server Failure\":\n                                    backgroundColor = \"rgba(217, 83, 79, 0.1)\";\n                                    borderColor = \"rgb(217, 83, 79)\";\n                                    break;\n\n                                case \"NX Domain\":\n                                    backgroundColor = \"rgba(120, 120, 120, 0.1)\";\n                                    borderColor = \"rgb(120, 120, 120)\";\n                                    break;\n\n                                case \"Refused\":\n                                    backgroundColor = \"rgba(91, 192, 222, 0.1)\";\n                                    borderColor = \"rgb(91, 192, 222)\";\n                                    break;\n\n                                case \"Authoritative\":\n                                    backgroundColor = \"rgba(150, 150, 0, 0.1)\";\n                                    borderColor = \"rgb(150, 150, 0)\";\n                                    break;\n\n                                case \"Recursive\":\n                                    backgroundColor = \"rgba(23, 162, 184, 0.1)\";\n                                    borderColor = \"rgb(23, 162, 184)\";\n                                    break;\n\n                                case \"Cached\":\n                                    backgroundColor = \"rgba(111, 84, 153, 0.1)\";\n                                    borderColor = \"rgb(111, 84, 153)\";\n                                    break;\n\n                                case \"Blocked\":\n                                    backgroundColor = \"rgba(255, 165, 0, 0.1)\";\n                                    borderColor = \"rgb(255, 165, 0)\";\n                                    break;\n\n                                case \"Dropped\":\n                                    backgroundColor = \"rgba(30, 30, 30, 0.1)\";\n                                    borderColor = \"rgb(30, 30, 30)\";\n                                    break;\n\n                                case \"Clients\":\n                                    backgroundColor = \"rgba(51, 122, 183, 0.1)\";\n                                    borderColor = \"rgb(51, 122, 183)\";\n                                    break;\n\n                                default:\n                                    throw new InvalidOperationException();\n                            }\n\n                            WriteChartDataSet(jsonWriter, dataSet, backgroundColor, borderColor);\n                        }\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                //query response chart\n                {\n                    jsonWriter.WritePropertyName(\"queryResponseChartData\");\n                    jsonWriter.WriteStartObject();\n\n                    //labels\n                    {\n                        jsonWriter.WritePropertyName(\"labels\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (string label in dashboardStats.QueryResponseChartData.Labels)\n                            jsonWriter.WriteStringValue(label);\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    //datasets\n                    {\n                        jsonWriter.WritePropertyName(\"datasets\");\n                        jsonWriter.WriteStartArray();\n\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WritePropertyName(\"data\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (long value in dashboardStats.QueryResponseChartData.DataSets[0].Data)\n                            jsonWriter.WriteNumberValue(value);\n\n                        jsonWriter.WriteEndArray();\n\n                        jsonWriter.WritePropertyName(\"backgroundColor\");\n                        jsonWriter.WriteStartArray();\n                        jsonWriter.WriteStringValue(\"rgba(150, 150, 0, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(23, 162, 184, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(111, 84, 153, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(255, 165, 0, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(7, 7, 7, 0.5)\");\n                        jsonWriter.WriteEndArray();\n\n                        jsonWriter.WriteEndObject();\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                //query type chart\n                {\n                    jsonWriter.WritePropertyName(\"queryTypeChartData\");\n                    jsonWriter.WriteStartObject();\n\n                    //labels\n                    {\n                        jsonWriter.WritePropertyName(\"labels\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (string label in dashboardStats.QueryTypeChartData.Labels)\n                            jsonWriter.WriteStringValue(label);\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    //datasets\n                    {\n                        jsonWriter.WritePropertyName(\"datasets\");\n                        jsonWriter.WriteStartArray();\n\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WritePropertyName(\"data\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (long value in dashboardStats.QueryTypeChartData.DataSets[0].Data)\n                            jsonWriter.WriteNumberValue(value);\n\n                        jsonWriter.WriteEndArray();\n\n                        jsonWriter.WritePropertyName(\"backgroundColor\");\n                        jsonWriter.WriteStartArray();\n                        jsonWriter.WriteStringValue(\"rgba(102, 153, 255, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(92, 184, 92, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(7, 7, 7, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(91, 192, 222, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(150, 150, 0, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(23, 162, 184, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(111, 84, 153, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(255, 165, 0, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(51, 122, 183, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(150, 150, 150, 0.5)\");\n                        jsonWriter.WriteEndArray();\n\n                        jsonWriter.WriteEndObject();\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                //protocol type chart\n                {\n                    jsonWriter.WritePropertyName(\"protocolTypeChartData\");\n                    jsonWriter.WriteStartObject();\n\n                    //labels\n                    {\n                        jsonWriter.WritePropertyName(\"labels\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (string label in dashboardStats.ProtocolTypeChartData.Labels)\n                            jsonWriter.WriteStringValue(label);\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    //datasets\n                    {\n                        jsonWriter.WritePropertyName(\"datasets\");\n                        jsonWriter.WriteStartArray();\n\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WritePropertyName(\"data\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (long value in dashboardStats.ProtocolTypeChartData.DataSets[0].Data)\n                            jsonWriter.WriteNumberValue(value);\n\n                        jsonWriter.WriteEndArray();\n\n                        jsonWriter.WritePropertyName(\"backgroundColor\");\n                        jsonWriter.WriteStartArray();\n                        jsonWriter.WriteStringValue(\"rgba(111, 84, 153, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(150, 150, 0, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(23, 162, 184, 0.5)\"); ;\n                        jsonWriter.WriteStringValue(\"rgba(255, 165, 0, 0.5)\");\n                        jsonWriter.WriteStringValue(\"rgba(91, 192, 222, 0.5)\");\n                        jsonWriter.WriteEndArray();\n\n                        jsonWriter.WriteEndObject();\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                //top clients\n                {\n                    await ResolvePtrTopClientsAsync(dashboardStats.TopClients);\n\n                    jsonWriter.WritePropertyName(\"topClients\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (DashboardStats.TopClientStats item in dashboardStats.TopClients)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"name\", item.Name);\n\n                        if (!string.IsNullOrEmpty(item.Domain))\n                            jsonWriter.WriteString(\"domain\", item.Domain);\n\n                        jsonWriter.WriteNumber(\"hits\", item.Hits);\n\n                        IPAddress ip = IPAddress.Parse(item.Name);\n                        jsonWriter.WriteBoolean(\"rateLimited\", item.RateLimited || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Udp) || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Tcp));\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                //top domains\n                {\n                    jsonWriter.WritePropertyName(\"topDomains\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (DashboardStats.TopStats item in dashboardStats.TopDomains)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"name\", item.Name);\n\n                        if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn))\n                            jsonWriter.WriteString(\"nameIdn\", idn);\n\n                        jsonWriter.WriteNumber(\"hits\", item.Hits);\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                //top blocked domains\n                {\n                    jsonWriter.WritePropertyName(\"topBlockedDomains\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (DashboardStats.TopStats item in dashboardStats.TopBlockedDomains)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"name\", item.Name);\n\n                        if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn))\n                            jsonWriter.WriteString(\"nameIdn\", idn);\n\n                        jsonWriter.WriteNumber(\"hits\", item.Hits);\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n            }\n\n            public async Task GetTopStats(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                DashboardStatsType type = request.GetQueryOrFormEnum(\"type\", DashboardStatsType.LastHour);\n                DashboardTopStatsType statsType = request.GetQueryOrFormEnum<DashboardTopStatsType>(\"statsType\");\n                int limit = request.GetQueryOrForm(\"limit\", int.Parse, 1000);\n\n                DateTime startDate = default;\n                DateTime endDate = default;\n\n                if (type == DashboardStatsType.Custom)\n                {\n                    string strStartDate = request.GetQueryOrForm(\"start\");\n                    string strEndDate = request.GetQueryOrForm(\"end\");\n\n                    if (!DateTime.TryParse(strStartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out startDate))\n                        throw new DnsWebServiceException(\"Invalid start date format.\");\n\n                    if (!DateTime.TryParse(strEndDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out endDate))\n                        throw new DnsWebServiceException(\"Invalid end date format.\");\n\n                    if (startDate > endDate)\n                        throw new DnsWebServiceException(\"Start date must be less than or equal to end date.\");\n                }\n\n                List<Task<DashboardStats>> tasks = null;\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    string node = request.GetQueryOrForm(\"node\", null);\n                    if (\"cluster\".Equals(node, StringComparison.OrdinalIgnoreCase))\n                    {\n                        IReadOnlyDictionary<int, Cluster.ClusterNode> clusterNodes = _dnsWebService._clusterManager.ClusterNodes;\n                        tasks = new List<Task<DashboardStats>>(clusterNodes.Count);\n\n                        foreach (KeyValuePair<int, Cluster.ClusterNode> clusterNode in clusterNodes)\n                        {\n                            if (clusterNode.Value.State == Cluster.ClusterNodeState.Self)\n                                continue;\n\n                            tasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1)\n                            {\n                                return clusterNode.Value.GetDashboardTopStatsAsync(sessionUser, statsType, limit, type, startDate, endDate, cancellationToken1);\n                            }, CLUSTER_NODE_DASHBOARD_STATS_API_TIMEOUT));\n                        }\n                    }\n                }\n\n                DashboardStats topStatsData;\n\n                switch (type)\n                {\n                    case DashboardStatsType.LastHour:\n                        topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastHourTopStats(statsType, limit);\n                        break;\n\n                    case DashboardStatsType.LastDay:\n                        topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastDayTopStats(statsType, limit);\n                        break;\n\n                    case DashboardStatsType.LastWeek:\n                        topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastWeekTopStats(statsType, limit);\n                        break;\n\n                    case DashboardStatsType.LastMonth:\n                        topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastMonthTopStats(statsType, limit);\n                        break;\n\n                    case DashboardStatsType.LastYear:\n                        topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastYearTopStats(statsType, limit);\n                        break;\n\n                    case DashboardStatsType.Custom:\n                        TimeSpan duration = endDate - startDate;\n\n                        if ((Convert.ToInt32(duration.TotalDays) + 1) > 7)\n                            topStatsData = _dnsWebService._dnsServer.StatsManager.GetDayWiseTopStats(startDate, endDate, statsType, limit);\n                        else if ((Convert.ToInt32(duration.TotalHours) + 1) > 3)\n                            topStatsData = _dnsWebService._dnsServer.StatsManager.GetHourWiseTopStats(startDate, endDate, statsType, limit);\n                        else\n                            topStatsData = _dnsWebService._dnsServer.StatsManager.GetMinuteWiseTopStats(startDate, endDate, statsType, limit);\n\n                        break;\n\n                    default:\n                        throw new DnsWebServiceException(\"Unknown stats type requested: \" + type.ToString());\n                }\n\n                if (tasks is not null)\n                {\n                    foreach (Task<DashboardStats> task in tasks)\n                    {\n                        try\n                        {\n                            topStatsData.Merge(await task, limit);\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(ex);\n                        }\n                    }\n                }\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                switch (statsType)\n                {\n                    case DashboardTopStatsType.TopClients:\n                        {\n                            bool noReverseLookup = request.GetQueryOrForm(\"noReverseLookup\", bool.Parse, false);\n                            bool onlyRateLimitedClients = request.GetQueryOrForm(\"onlyRateLimitedClients\", bool.Parse, false);\n\n                            if (!noReverseLookup)\n                                await ResolvePtrTopClientsAsync(topStatsData.TopClients);\n\n                            jsonWriter.WritePropertyName(\"topClients\");\n                            jsonWriter.WriteStartArray();\n\n                            foreach (DashboardStats.TopClientStats item in topStatsData.TopClients)\n                            {\n                                IPAddress ip = IPAddress.Parse(item.Name);\n                                bool rateLimited = item.RateLimited || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Udp) || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Tcp);\n\n                                if (onlyRateLimitedClients && !rateLimited)\n                                    continue;\n\n                                jsonWriter.WriteStartObject();\n\n                                jsonWriter.WriteString(\"name\", item.Name);\n\n                                if (!string.IsNullOrEmpty(item.Domain))\n                                    jsonWriter.WriteString(\"domain\", item.Domain);\n\n                                jsonWriter.WriteNumber(\"hits\", item.Hits);\n                                jsonWriter.WriteBoolean(\"rateLimited\", rateLimited);\n\n                                jsonWriter.WriteEndObject();\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n\n                    case DashboardTopStatsType.TopDomains:\n                        {\n                            jsonWriter.WritePropertyName(\"topDomains\");\n                            jsonWriter.WriteStartArray();\n\n                            foreach (DashboardStats.TopStats item in topStatsData.TopDomains)\n                            {\n                                jsonWriter.WriteStartObject();\n\n                                jsonWriter.WriteString(\"name\", item.Name);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn))\n                                    jsonWriter.WriteString(\"nameIdn\", idn);\n\n                                jsonWriter.WriteNumber(\"hits\", item.Hits);\n\n                                jsonWriter.WriteEndObject();\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n\n                    case DashboardTopStatsType.TopBlockedDomains:\n                        {\n                            jsonWriter.WritePropertyName(\"topBlockedDomains\");\n                            jsonWriter.WriteStartArray();\n\n                            foreach (DashboardStats.TopStats item in topStatsData.TopBlockedDomains)\n                            {\n                                jsonWriter.WriteStartObject();\n\n                                jsonWriter.WriteString(\"name\", item.Name);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn))\n                                    jsonWriter.WriteString(\"nameIdn\", idn);\n\n                                jsonWriter.WriteNumber(\"hits\", item.Hits);\n\n                                jsonWriter.WriteEndObject();\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n\n                    default:\n                        throw new NotSupportedException();\n                }\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceDhcpApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Dhcp;\nusing DnsServerCore.Dhcp.Options;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        class WebServiceDhcpApi\n        {\n            #region variables\n\n            static readonly char[] _commaSeparator = new char[] { ',' };\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceDhcpApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region public\n\n            public void ListDhcpLeases(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                IReadOnlyDictionary<string, Scope> scopes = _dnsWebService._dhcpServer.Scopes;\n\n                //sort by name\n                List<Scope> sortedScopes = new List<Scope>(scopes.Count);\n\n                foreach (KeyValuePair<string, Scope> entry in scopes)\n                    sortedScopes.Add(entry.Value);\n\n                sortedScopes.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"leases\");\n                jsonWriter.WriteStartArray();\n\n                foreach (Scope scope in sortedScopes)\n                {\n                    IReadOnlyDictionary<ClientIdentifierOption, Lease> leases = scope.Leases;\n\n                    //sort by address\n                    List<Lease> sortedLeases = new List<Lease>(leases.Count);\n\n                    foreach (KeyValuePair<ClientIdentifierOption, Lease> entry in leases)\n                        sortedLeases.Add(entry.Value);\n\n                    sortedLeases.Sort();\n\n                    foreach (Lease lease in sortedLeases)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"scope\", scope.Name);\n                        jsonWriter.WriteString(\"type\", lease.Type.ToString());\n                        jsonWriter.WriteString(\"hardwareAddress\", BitConverter.ToString(lease.HardwareAddress));\n                        jsonWriter.WriteString(\"clientIdentifier\", lease.ClientIdentifier.ToString());\n                        jsonWriter.WriteString(\"address\", lease.Address.ToString());\n                        jsonWriter.WriteString(\"hostName\", lease.HostName);\n                        jsonWriter.WriteString(\"leaseObtained\", lease.LeaseObtained);\n                        jsonWriter.WriteString(\"leaseExpires\", lease.LeaseExpires);\n\n                        jsonWriter.WriteEndObject();\n                    }\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void ListDhcpScopes(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                IReadOnlyDictionary<string, Scope> scopes = _dnsWebService._dhcpServer.Scopes;\n\n                //sort by name\n                List<Scope> sortedScopes = new List<Scope>(scopes.Count);\n\n                foreach (KeyValuePair<string, Scope> entry in scopes)\n                    sortedScopes.Add(entry.Value);\n\n                sortedScopes.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"scopes\");\n                jsonWriter.WriteStartArray();\n\n                foreach (Scope scope in sortedScopes)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"name\", scope.Name);\n                    jsonWriter.WriteBoolean(\"enabled\", scope.Enabled);\n                    jsonWriter.WriteString(\"startingAddress\", scope.StartingAddress.ToString());\n                    jsonWriter.WriteString(\"endingAddress\", scope.EndingAddress.ToString());\n                    jsonWriter.WriteString(\"subnetMask\", scope.SubnetMask.ToString());\n                    jsonWriter.WriteString(\"networkAddress\", scope.NetworkAddress.ToString());\n                    jsonWriter.WriteString(\"broadcastAddress\", scope.BroadcastAddress.ToString());\n\n                    if (scope.InterfaceAddress is not null)\n                        jsonWriter.WriteString(\"interfaceAddress\", scope.InterfaceAddress.ToString());\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void GetDhcpScope(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string scopeName = context.Request.GetQueryOrForm(\"name\");\n\n                Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName);\n                if (scope is null)\n                    throw new DnsWebServiceException(\"DHCP scope was not found: \" + scopeName);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"name\", scope.Name);\n                jsonWriter.WriteString(\"startingAddress\", scope.StartingAddress.ToString());\n                jsonWriter.WriteString(\"endingAddress\", scope.EndingAddress.ToString());\n                jsonWriter.WriteString(\"subnetMask\", scope.SubnetMask.ToString());\n                jsonWriter.WriteNumber(\"leaseTimeDays\", scope.LeaseTimeDays);\n                jsonWriter.WriteNumber(\"leaseTimeHours\", scope.LeaseTimeHours);\n                jsonWriter.WriteNumber(\"leaseTimeMinutes\", scope.LeaseTimeMinutes);\n                jsonWriter.WriteNumber(\"offerDelayTime\", scope.OfferDelayTime);\n\n                jsonWriter.WriteBoolean(\"pingCheckEnabled\", scope.PingCheckEnabled);\n                jsonWriter.WriteNumber(\"pingCheckTimeout\", scope.PingCheckTimeout);\n                jsonWriter.WriteNumber(\"pingCheckRetries\", scope.PingCheckRetries);\n\n                if (!string.IsNullOrEmpty(scope.DomainName))\n                    jsonWriter.WriteString(\"domainName\", scope.DomainName);\n\n                if (scope.DomainSearchList is not null)\n                {\n                    jsonWriter.WritePropertyName(\"domainSearchList\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (string domainSearchString in scope.DomainSearchList)\n                        jsonWriter.WriteStringValue(domainSearchString);\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                jsonWriter.WriteBoolean(\"dnsUpdates\", scope.DnsUpdates);\n                jsonWriter.WriteBoolean(\"dnsOverwriteForDynamicLease\", scope.DnsOverwriteForDynamicLease);\n                jsonWriter.WriteNumber(\"dnsTtl\", scope.DnsTtl);\n\n                if (scope.ServerAddress is not null)\n                    jsonWriter.WriteString(\"serverAddress\", scope.ServerAddress.ToString());\n\n                if (scope.ServerHostName is not null)\n                    jsonWriter.WriteString(\"serverHostName\", scope.ServerHostName);\n\n                if (scope.BootFileName is not null)\n                    jsonWriter.WriteString(\"bootFileName\", scope.BootFileName);\n\n                if (scope.RouterAddress is not null)\n                    jsonWriter.WriteString(\"routerAddress\", scope.RouterAddress.ToString());\n\n                jsonWriter.WriteBoolean(\"useThisDnsServer\", scope.UseThisDnsServer);\n\n                if (scope.DnsServers is not null)\n                {\n                    jsonWriter.WritePropertyName(\"dnsServers\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (IPAddress dnsServer in scope.DnsServers)\n                        jsonWriter.WriteStringValue(dnsServer.ToString());\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.WinsServers is not null)\n                {\n                    jsonWriter.WritePropertyName(\"winsServers\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (IPAddress winsServer in scope.WinsServers)\n                        jsonWriter.WriteStringValue(winsServer.ToString());\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.NtpServers is not null)\n                {\n                    jsonWriter.WritePropertyName(\"ntpServers\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (IPAddress ntpServer in scope.NtpServers)\n                        jsonWriter.WriteStringValue(ntpServer.ToString());\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.NtpServerDomainNames is not null)\n                {\n                    jsonWriter.WritePropertyName(\"ntpServerDomainNames\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (string ntpServerDomainName in scope.NtpServerDomainNames)\n                        jsonWriter.WriteStringValue(ntpServerDomainName);\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.StaticRoutes is not null)\n                {\n                    jsonWriter.WritePropertyName(\"staticRoutes\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (ClasslessStaticRouteOption.Route route in scope.StaticRoutes)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"destination\", route.Destination.ToString());\n                        jsonWriter.WriteString(\"subnetMask\", route.SubnetMask.ToString());\n                        jsonWriter.WriteString(\"router\", route.Router.ToString());\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.VendorInfo is not null)\n                {\n                    jsonWriter.WritePropertyName(\"vendorInfo\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (KeyValuePair<string, VendorSpecificInformationOption> entry in scope.VendorInfo)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"identifier\", entry.Key);\n                        jsonWriter.WriteString(\"information\", BitConverter.ToString(entry.Value.Information).Replace('-', ':'));\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.CAPWAPAcIpAddresses is not null)\n                {\n                    jsonWriter.WritePropertyName(\"capwapAcIpAddresses\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (IPAddress acIpAddress in scope.CAPWAPAcIpAddresses)\n                        jsonWriter.WriteStringValue(acIpAddress.ToString());\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.TftpServerAddresses is not null)\n                {\n                    jsonWriter.WritePropertyName(\"tftpServerAddresses\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (IPAddress address in scope.TftpServerAddresses)\n                        jsonWriter.WriteStringValue(address.ToString());\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.GenericOptions is not null)\n                {\n                    jsonWriter.WritePropertyName(\"genericOptions\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (DhcpOption genericOption in scope.GenericOptions)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteNumber(\"code\", (byte)genericOption.Code);\n                        jsonWriter.WriteString(\"value\", BitConverter.ToString(genericOption.RawValue).Replace('-', ':'));\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (scope.Exclusions is not null)\n                {\n                    jsonWriter.WritePropertyName(\"exclusions\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (Exclusion exclusion in scope.Exclusions)\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"startingAddress\", exclusion.StartingAddress.ToString());\n                        jsonWriter.WriteString(\"endingAddress\", exclusion.EndingAddress.ToString());\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                jsonWriter.WritePropertyName(\"reservedLeases\");\n                jsonWriter.WriteStartArray();\n\n                foreach (Lease reservedLease in scope.ReservedLeases)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"hostName\", reservedLease.HostName);\n                    jsonWriter.WriteString(\"hardwareAddress\", BitConverter.ToString(reservedLease.HardwareAddress));\n                    jsonWriter.WriteString(\"address\", reservedLease.Address.ToString());\n                    jsonWriter.WriteString(\"comments\", reservedLease.Comments);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WriteBoolean(\"allowOnlyReservedLeases\", scope.AllowOnlyReservedLeases);\n                jsonWriter.WriteBoolean(\"blockLocallyAdministeredMacAddresses\", scope.BlockLocallyAdministeredMacAddresses);\n                jsonWriter.WriteBoolean(\"ignoreClientIdentifierOption\", scope.IgnoreClientIdentifierOption);\n            }\n\n            public async Task SetDhcpScopeAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string scopeName = request.GetQueryOrForm(\"name\");\n                string strStartingAddress = request.QueryOrForm(\"startingAddress\");\n                string strEndingAddress = request.QueryOrForm(\"endingAddress\");\n                string strSubnetMask = request.QueryOrForm(\"subnetMask\");\n\n                bool scopeExists;\n                Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName);\n                if (scope is null)\n                {\n                    //scope does not exists; create new scope\n                    if (string.IsNullOrEmpty(strStartingAddress))\n                        throw new DnsWebServiceException(\"Parameter 'startingAddress' missing.\");\n\n                    if (string.IsNullOrEmpty(strEndingAddress))\n                        throw new DnsWebServiceException(\"Parameter 'endingAddress' missing.\");\n\n                    if (string.IsNullOrEmpty(strSubnetMask))\n                        throw new DnsWebServiceException(\"Parameter 'subnetMask' missing.\");\n\n                    scopeExists = false;\n                    scope = new Scope(scopeName, true, IPAddress.Parse(strStartingAddress), IPAddress.Parse(strEndingAddress), IPAddress.Parse(strSubnetMask), _dnsWebService._log, _dnsWebService._dhcpServer);\n                    scope.IgnoreClientIdentifierOption = true;\n                }\n                else\n                {\n                    scopeExists = true;\n\n                    IPAddress startingAddress = string.IsNullOrEmpty(strStartingAddress) ? scope.StartingAddress : IPAddress.Parse(strStartingAddress);\n                    IPAddress endingAddress = string.IsNullOrEmpty(strEndingAddress) ? scope.EndingAddress : IPAddress.Parse(strEndingAddress);\n                    IPAddress subnetMask = string.IsNullOrEmpty(strSubnetMask) ? scope.SubnetMask : IPAddress.Parse(strSubnetMask);\n\n                    //validate scope address\n                    foreach (KeyValuePair<string, Scope> entry in _dnsWebService._dhcpServer.Scopes)\n                    {\n                        Scope existingScope = entry.Value;\n\n                        if (existingScope.Equals(scope))\n                            continue;\n\n                        if (existingScope.IsAddressInRange(startingAddress) || existingScope.IsAddressInRange(endingAddress))\n                            throw new DhcpServerException(\"Scope with overlapping range already exists: \" + existingScope.StartingAddress.ToString() + \"-\" + existingScope.EndingAddress.ToString());\n                    }\n\n                    scope.ChangeNetwork(startingAddress, endingAddress, subnetMask);\n                }\n\n                if (request.TryGetQueryOrForm(\"leaseTimeDays\", ushort.Parse, out ushort leaseTimeDays))\n                    scope.LeaseTimeDays = leaseTimeDays;\n\n                if (request.TryGetQueryOrForm(\"leaseTimeHours\", byte.Parse, out byte leaseTimeHours))\n                    scope.LeaseTimeHours = leaseTimeHours;\n\n                if (request.TryGetQueryOrForm(\"leaseTimeMinutes\", byte.Parse, out byte leaseTimeMinutes))\n                    scope.LeaseTimeMinutes = leaseTimeMinutes;\n\n                if (request.TryGetQueryOrForm(\"offerDelayTime\", ushort.Parse, out ushort offerDelayTime))\n                    scope.OfferDelayTime = offerDelayTime;\n\n                if (request.TryGetQueryOrForm(\"pingCheckEnabled\", bool.Parse, out bool pingCheckEnabled))\n                    scope.PingCheckEnabled = pingCheckEnabled;\n\n                if (request.TryGetQueryOrForm(\"pingCheckTimeout\", ushort.Parse, out ushort pingCheckTimeout))\n                    scope.PingCheckTimeout = pingCheckTimeout;\n\n                if (request.TryGetQueryOrForm(\"pingCheckRetries\", byte.Parse, out byte pingCheckRetries))\n                    scope.PingCheckRetries = pingCheckRetries;\n\n                string domainName = request.QueryOrForm(\"domainName\");\n                if (domainName is not null)\n                    scope.DomainName = domainName.Length == 0 ? null : domainName;\n\n                string domainSearchList = request.QueryOrForm(\"domainSearchList\");\n                if (domainSearchList is not null)\n                {\n                    if (domainSearchList.Length == 0)\n                        scope.DomainSearchList = null;\n                    else\n                        scope.DomainSearchList = domainSearchList.Split(_commaSeparator, StringSplitOptions.RemoveEmptyEntries);\n                }\n\n                if (request.TryGetQueryOrForm(\"dnsUpdates\", bool.Parse, out bool dnsUpdates))\n                    scope.DnsUpdates = dnsUpdates;\n\n                if (request.TryGetQueryOrForm(\"dnsOverwriteForDynamicLease\", bool.Parse, out bool dnsOverwriteForDynamicLease))\n                    scope.DnsOverwriteForDynamicLease = dnsOverwriteForDynamicLease;\n\n                if (request.TryGetQueryOrForm(\"dnsTtl\", ZoneFile.ParseTtl, out uint dnsTtl))\n                    scope.DnsTtl = dnsTtl;\n\n                string serverAddress = request.QueryOrForm(\"serverAddress\");\n                if (serverAddress is not null)\n                    scope.ServerAddress = serverAddress.Length == 0 ? null : IPAddress.Parse(serverAddress);\n\n                string serverHostName = request.QueryOrForm(\"serverHostName\");\n                if (serverHostName is not null)\n                    scope.ServerHostName = serverHostName.Length == 0 ? null : serverHostName;\n\n                string bootFileName = request.QueryOrForm(\"bootFileName\");\n                if (bootFileName is not null)\n                    scope.BootFileName = bootFileName.Length == 0 ? null : bootFileName;\n\n                string routerAddress = request.QueryOrForm(\"routerAddress\");\n                if (routerAddress is not null)\n                    scope.RouterAddress = routerAddress.Length == 0 ? null : IPAddress.Parse(routerAddress);\n\n                if (request.TryGetQueryOrForm(\"useThisDnsServer\", bool.Parse, out bool useThisDnsServer))\n                    scope.UseThisDnsServer = useThisDnsServer;\n\n                if (!scope.UseThisDnsServer)\n                {\n                    string dnsServers = request.QueryOrForm(\"dnsServers\");\n                    if (dnsServers is not null)\n                    {\n                        if (dnsServers.Length == 0)\n                            scope.DnsServers = null;\n                        else\n                            scope.DnsServers = dnsServers.Split(IPAddress.Parse, ',');\n                    }\n                }\n\n                string winsServers = request.QueryOrForm(\"winsServers\");\n                if (winsServers is not null)\n                {\n                    if (winsServers.Length == 0)\n                        scope.WinsServers = null;\n                    else\n                        scope.WinsServers = winsServers.Split(IPAddress.Parse, ',');\n                }\n\n                string ntpServers = request.QueryOrForm(\"ntpServers\");\n                if (ntpServers is not null)\n                {\n                    if (ntpServers.Length == 0)\n                        scope.NtpServers = null;\n                    else\n                        scope.NtpServers = ntpServers.Split(IPAddress.Parse, ',');\n                }\n\n                string ntpServerDomainNames = request.QueryOrForm(\"ntpServerDomainNames\");\n                if (ntpServerDomainNames is not null)\n                {\n                    if (ntpServerDomainNames.Length == 0)\n                        scope.NtpServerDomainNames = null;\n                    else\n                        scope.NtpServerDomainNames = ntpServerDomainNames.Split(_commaSeparator, StringSplitOptions.RemoveEmptyEntries);\n                }\n\n                string strStaticRoutes = request.QueryOrForm(\"staticRoutes\");\n                if (strStaticRoutes is not null)\n                {\n                    if (strStaticRoutes.Length == 0)\n                    {\n                        scope.StaticRoutes = null;\n                    }\n                    else\n                    {\n                        string[] strStaticRoutesParts = strStaticRoutes.Split('|');\n                        List<ClasslessStaticRouteOption.Route> staticRoutes = new List<ClasslessStaticRouteOption.Route>();\n\n                        for (int i = 0; i < strStaticRoutesParts.Length; i += 3)\n                            staticRoutes.Add(new ClasslessStaticRouteOption.Route(IPAddress.Parse(strStaticRoutesParts[i + 0]), IPAddress.Parse(strStaticRoutesParts[i + 1]), IPAddress.Parse(strStaticRoutesParts[i + 2])));\n\n                        scope.StaticRoutes = staticRoutes;\n                    }\n                }\n\n                string strVendorInfo = request.QueryOrForm(\"vendorInfo\");\n                if (strVendorInfo is not null)\n                {\n                    if (strVendorInfo.Length == 0)\n                    {\n                        scope.VendorInfo = null;\n                    }\n                    else\n                    {\n                        string[] strVendorInfoParts = strVendorInfo.Split('|');\n                        Dictionary<string, VendorSpecificInformationOption> vendorInfo = new Dictionary<string, VendorSpecificInformationOption>();\n\n                        for (int i = 0; i < strVendorInfoParts.Length; i += 2)\n                            vendorInfo.Add(strVendorInfoParts[i + 0], new VendorSpecificInformationOption(strVendorInfoParts[i + 1]));\n\n                        scope.VendorInfo = vendorInfo;\n                    }\n                }\n\n                string capwapAcIpAddresses = request.QueryOrForm(\"capwapAcIpAddresses\");\n                if (capwapAcIpAddresses is not null)\n                {\n                    if (capwapAcIpAddresses.Length == 0)\n                        scope.CAPWAPAcIpAddresses = null;\n                    else\n                        scope.CAPWAPAcIpAddresses = capwapAcIpAddresses.Split(IPAddress.Parse, ',');\n                }\n\n                string tftpServerAddresses = request.QueryOrForm(\"tftpServerAddresses\");\n                if (tftpServerAddresses is not null)\n                {\n                    if (tftpServerAddresses.Length == 0)\n                        scope.TftpServerAddresses = null;\n                    else\n                        scope.TftpServerAddresses = tftpServerAddresses.Split(IPAddress.Parse, ',');\n                }\n\n                string strGenericOptions = request.QueryOrForm(\"genericOptions\");\n                if (strGenericOptions is not null)\n                {\n                    if (strGenericOptions.Length == 0)\n                    {\n                        scope.GenericOptions = null;\n                    }\n                    else\n                    {\n                        string[] strGenericOptionsParts = strGenericOptions.Split('|');\n                        List<DhcpOption> genericOptions = new List<DhcpOption>();\n\n                        for (int i = 0; i < strGenericOptionsParts.Length; i += 2)\n                            genericOptions.Add(new DhcpOption((DhcpOptionCode)byte.Parse(strGenericOptionsParts[i + 0]), strGenericOptionsParts[i + 1]));\n\n                        scope.GenericOptions = genericOptions;\n                    }\n                }\n\n                string strExclusions = request.QueryOrForm(\"exclusions\");\n                if (strExclusions is not null)\n                {\n                    if (strExclusions.Length == 0)\n                    {\n                        scope.Exclusions = null;\n                    }\n                    else\n                    {\n                        string[] strExclusionsParts = strExclusions.Split('|');\n                        List<Exclusion> exclusions = new List<Exclusion>();\n\n                        for (int i = 0; i < strExclusionsParts.Length; i += 2)\n                            exclusions.Add(new Exclusion(IPAddress.Parse(strExclusionsParts[i + 0]), IPAddress.Parse(strExclusionsParts[i + 1])));\n\n                        scope.Exclusions = exclusions;\n                    }\n                }\n\n                string strReservedLeases = request.QueryOrForm(\"reservedLeases\");\n                if (strReservedLeases is not null)\n                {\n                    if (strReservedLeases.Length == 0)\n                    {\n                        scope.ReservedLeases = null;\n                    }\n                    else\n                    {\n                        string[] strReservedLeaseParts = strReservedLeases.Split('|');\n                        List<Lease> reservedLeases = new List<Lease>();\n\n                        for (int i = 0; i < strReservedLeaseParts.Length; i += 4)\n                            reservedLeases.Add(new Lease(LeaseType.Reserved, strReservedLeaseParts[i + 0], DhcpMessageHardwareAddressType.Ethernet, strReservedLeaseParts[i + 1], IPAddress.Parse(strReservedLeaseParts[i + 2]), strReservedLeaseParts[i + 3]));\n\n                        scope.ReservedLeases = reservedLeases;\n                    }\n                }\n\n                if (request.TryGetQueryOrForm(\"allowOnlyReservedLeases\", bool.Parse, out bool allowOnlyReservedLeases))\n                    scope.AllowOnlyReservedLeases = allowOnlyReservedLeases;\n\n                if (request.TryGetQueryOrForm(\"blockLocallyAdministeredMacAddresses\", bool.Parse, out bool blockLocallyAdministeredMacAddresses))\n                    scope.BlockLocallyAdministeredMacAddresses = blockLocallyAdministeredMacAddresses;\n\n                if (request.TryGetQueryOrForm(\"ignoreClientIdentifierOption\", bool.Parse, out bool ignoreClientIdentifierOption))\n                    scope.IgnoreClientIdentifierOption = ignoreClientIdentifierOption;\n\n                if (scopeExists)\n                {\n                    _dnsWebService._dhcpServer.SaveScope(scopeName);\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope was updated successfully: \" + scopeName);\n                }\n                else\n                {\n                    await _dnsWebService._dhcpServer.AddScopeAsync(scope);\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope was added successfully: \" + scopeName);\n                }\n\n                if (request.TryGetQueryOrForm(\"newName\", out string newName) && !newName.Equals(scopeName))\n                {\n                    _dnsWebService._dhcpServer.RenameScope(scopeName, newName);\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope was renamed successfully: '\" + scopeName + \"' to '\" + newName + \"'\");\n                }\n            }\n\n            public void AddReservedLease(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string scopeName = request.GetQueryOrForm(\"name\");\n\n                Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName);\n                if (scope is null)\n                    throw new DnsWebServiceException(\"No such scope exists: \" + scopeName);\n\n                string hostName = request.QueryOrForm(\"hostName\");\n                string hardwareAddress = request.GetQueryOrForm(\"hardwareAddress\");\n                string strIpAddress = request.GetQueryOrForm(\"ipAddress\");\n                string comments = request.QueryOrForm(\"comments\");\n\n                Lease reservedLease = new Lease(LeaseType.Reserved, hostName, DhcpMessageHardwareAddressType.Ethernet, hardwareAddress, IPAddress.Parse(strIpAddress), comments);\n\n                if (!scope.AddReservedLease(reservedLease))\n                    throw new DnsWebServiceException(\"Failed to add reserved lease for scope: \" + scopeName);\n\n                _dnsWebService._dhcpServer.SaveScope(scopeName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope reserved lease was added successfully: \" + scopeName);\n            }\n\n            public void RemoveReservedLease(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string scopeName = request.GetQueryOrForm(\"name\");\n\n                Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName);\n                if (scope is null)\n                    throw new DnsWebServiceException(\"No such scope exists: \" + scopeName);\n\n                string hardwareAddress = request.GetQueryOrForm(\"hardwareAddress\");\n\n                if (!scope.RemoveReservedLease(hardwareAddress))\n                    throw new DnsWebServiceException(\"Failed to remove reserved lease for scope: \" + scopeName);\n\n                _dnsWebService._dhcpServer.SaveScope(scopeName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope reserved lease was removed successfully: \" + scopeName);\n            }\n\n            public async Task EnableDhcpScopeAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string scopeName = context.Request.GetQueryOrForm(\"name\");\n\n                await _dnsWebService._dhcpServer.EnableScopeAsync(scopeName, true);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope was enabled successfully: \" + scopeName);\n            }\n\n            public void DisableDhcpScope(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string scopeName = context.Request.GetQueryOrForm(\"name\");\n\n                _dnsWebService._dhcpServer.DisableScope(scopeName, true);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope was disabled successfully: \" + scopeName);\n            }\n\n            public void DeleteDhcpScope(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string scopeName = context.Request.GetQueryOrForm(\"name\");\n\n                _dnsWebService._dhcpServer.DeleteScope(scopeName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope was deleted successfully: \" + scopeName);\n            }\n\n            public void RemoveDhcpLease(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string scopeName = request.GetQueryOrForm(\"name\");\n\n                Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName);\n                if (scope is null)\n                    throw new DnsWebServiceException(\"DHCP scope does not exists: \" + scopeName);\n\n                string clientIdentifier = request.QueryOrForm(\"clientIdentifier\");\n                string hardwareAddress = request.QueryOrForm(\"hardwareAddress\");\n\n                if (!string.IsNullOrEmpty(clientIdentifier))\n                    scope.RemoveLease(ClientIdentifierOption.Parse(clientIdentifier));\n                else if (!string.IsNullOrEmpty(hardwareAddress))\n                    scope.RemoveLease(hardwareAddress);\n                else\n                    throw new DnsWebServiceException(\"Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified.\");\n\n                _dnsWebService._dhcpServer.SaveScope(scopeName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope's lease was removed successfully: \" + scopeName);\n            }\n\n            public void ConvertToReservedLease(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string scopeName = request.GetQueryOrForm(\"name\");\n\n                Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName);\n                if (scope == null)\n                    throw new DnsWebServiceException(\"DHCP scope does not exists: \" + scopeName);\n\n                string clientIdentifier = request.QueryOrForm(\"clientIdentifier\");\n                string hardwareAddress = request.QueryOrForm(\"hardwareAddress\");\n\n                if (!string.IsNullOrEmpty(clientIdentifier))\n                    scope.ConvertToReservedLease(ClientIdentifierOption.Parse(clientIdentifier));\n                else if (!string.IsNullOrEmpty(hardwareAddress))\n                    scope.ConvertToReservedLease(hardwareAddress);\n                else\n                    throw new DnsWebServiceException(\"Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified.\");\n\n                _dnsWebService._dhcpServer.SaveScope(scopeName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope's lease was reserved successfully: \" + scopeName);\n            }\n\n            public void ConvertToDynamicLease(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string scopeName = request.GetQueryOrForm(\"name\");\n\n                Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName);\n                if (scope == null)\n                    throw new DnsWebServiceException(\"DHCP scope does not exists: \" + scopeName);\n\n                string clientIdentifier = request.QueryOrForm(\"clientIdentifier\");\n                string hardwareAddress = request.QueryOrForm(\"hardwareAddress\");\n\n                if (!string.IsNullOrEmpty(clientIdentifier))\n                    scope.ConvertToDynamicLease(ClientIdentifierOption.Parse(clientIdentifier));\n                else if (!string.IsNullOrEmpty(hardwareAddress))\n                    scope.ConvertToDynamicLease(hardwareAddress);\n                else\n                    throw new DnsWebServiceException(\"Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified.\");\n\n                _dnsWebService._dhcpServer.SaveScope(scopeName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DHCP scope's lease was unreserved successfully: \" + scopeName);\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceLogsApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.ApplicationCommon;\nusing DnsServerCore.Auth;\nusing DnsServerCore.Dns.Applications;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Globalization;\nusing System.IO;\nusing System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        class WebServiceLogsApi\n        {\n            #region variables\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceLogsApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region public\n\n            public void ListLogs(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string[] logFiles = _dnsWebService._log.ListLogFiles();\n\n                Array.Sort(logFiles);\n                Array.Reverse(logFiles);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"logFiles\");\n                jsonWriter.WriteStartArray();\n\n                foreach (string logFile in logFiles)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteString(\"fileName\", Path.GetFileNameWithoutExtension(logFile));\n                    jsonWriter.WriteString(\"size\", WebUtilities.GetFormattedSize(new FileInfo(logFile).Length));\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public Task DownloadLogAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string fileName = request.GetQueryOrForm(\"fileName\");\n                int limit = request.GetQueryOrForm(\"limit\", int.Parse, 0);\n\n                return _dnsWebService._log.DownloadLogFileAsync(context, fileName, limit * 1024 * 1024);\n            }\n\n            public void DeleteLog(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string log = request.GetQueryOrForm(\"log\");\n\n                _dnsWebService._log.DeleteLogFile(log);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Log file was deleted: \" + log);\n            }\n\n            public void DeleteAllLogs(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._log.DeleteAllLogFiles();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] All log files were deleted.\");\n            }\n\n            public void DeleteAllStats(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.StatsManager.DeleteAllStats();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] All stats files were deleted.\");\n            }\n\n            public async Task QueryLogsAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\");\n                string classPath = request.GetQueryOrForm(\"classPath\");\n\n                if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application))\n                    throw new DnsWebServiceException(\"DNS application was not found: \" + name);\n\n                if (!application.DnsQueryLogs.TryGetValue(classPath, out IDnsQueryLogs queryLogs))\n                    throw new DnsWebServiceException(\"DNS application '\" + classPath + \"' class path was not found: \" + name);\n\n                long pageNumber = request.GetQueryOrForm(\"pageNumber\", long.Parse, 1);\n                int entriesPerPage = request.GetQueryOrForm(\"entriesPerPage\", int.Parse, 25);\n                bool descendingOrder = request.GetQueryOrForm(\"descendingOrder\", bool.Parse, true);\n\n                DateTime? start = null;\n                string strStart = request.QueryOrForm(\"start\");\n                if (!string.IsNullOrEmpty(strStart))\n                    start = DateTime.Parse(strStart, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);\n\n                DateTime? end = null;\n                string strEnd = request.QueryOrForm(\"end\");\n                if (!string.IsNullOrEmpty(strEnd))\n                    end = DateTime.Parse(strEnd, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);\n\n                IPAddress clientIpAddress = request.GetQueryOrForm(\"clientIpAddress\", IPAddress.Parse, null);\n\n                DnsTransportProtocol? protocol = null;\n                string strProtocol = request.QueryOrForm(\"protocol\");\n                if (!string.IsNullOrEmpty(strProtocol))\n                    protocol = Enum.Parse<DnsTransportProtocol>(strProtocol, true);\n\n                DnsServerResponseType? responseType = null;\n                string strResponseType = request.QueryOrForm(\"responseType\");\n                if (!string.IsNullOrEmpty(strResponseType))\n                    responseType = Enum.Parse<DnsServerResponseType>(strResponseType, true);\n\n                DnsResponseCode? rcode = null;\n                string strRcode = request.QueryOrForm(\"rcode\");\n                if (!string.IsNullOrEmpty(strRcode))\n                    rcode = Enum.Parse<DnsResponseCode>(strRcode, true);\n\n                string qname = request.GetQueryOrForm(\"qname\", null);\n                if (qname is not null)\n                    qname = qname.TrimEnd('.');\n\n                DnsResourceRecordType? qtype = null;\n                string strQtype = request.QueryOrForm(\"qtype\");\n                if (!string.IsNullOrEmpty(strQtype))\n                    qtype = Enum.Parse<DnsResourceRecordType>(strQtype, true);\n\n                DnsClass? qclass = null;\n                string strQclass = request.QueryOrForm(\"qclass\");\n                if (!string.IsNullOrEmpty(strQclass))\n                    qclass = Enum.Parse<DnsClass>(strQclass, true);\n\n                DnsLogPage page = await queryLogs.QueryLogsAsync(pageNumber, entriesPerPage, descendingOrder, start, end, clientIpAddress, protocol, responseType, rcode, qname, qtype, qclass);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteNumber(\"pageNumber\", page.PageNumber);\n                jsonWriter.WriteNumber(\"totalPages\", page.TotalPages);\n                jsonWriter.WriteNumber(\"totalEntries\", page.TotalEntries);\n\n                jsonWriter.WritePropertyName(\"entries\");\n                jsonWriter.WriteStartArray();\n\n                foreach (DnsLogEntry entry in page.Entries)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteNumber(\"rowNumber\", entry.RowNumber);\n                    jsonWriter.WriteString(\"timestamp\", entry.Timestamp);\n                    jsonWriter.WriteString(\"clientIpAddress\", entry.ClientIpAddress.ToString());\n                    jsonWriter.WriteString(\"protocol\", entry.Protocol.ToString());\n                    jsonWriter.WriteString(\"responseType\", entry.ResponseType.ToString());\n\n                    if (entry.ResponseRtt.HasValue)\n                        jsonWriter.WriteNumber(\"responseRtt\", entry.ResponseRtt.Value);\n\n                    jsonWriter.WriteString(\"rcode\", entry.RCODE.ToString());\n                    jsonWriter.WriteString(\"qname\", entry.Question?.Name);\n                    jsonWriter.WriteString(\"qtype\", entry.Question?.Type.ToString());\n                    jsonWriter.WriteString(\"qclass\", entry.Question?.Class.ToString());\n                    jsonWriter.WriteString(\"answer\", entry.Answer);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public async Task ExportLogsAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string name = request.GetQueryOrForm(\"name\");\n                string classPath = request.GetQueryOrForm(\"classPath\");\n\n                if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application))\n                    throw new DnsWebServiceException(\"DNS application was not found: \" + name);\n\n                if (!application.DnsQueryLogs.TryGetValue(classPath, out IDnsQueryLogs queryLogs))\n                    throw new DnsWebServiceException(\"DNS application '\" + classPath + \"' class path was not found: \" + name);\n\n                DateTime? start = null;\n                string strStart = request.QueryOrForm(\"start\");\n                if (!string.IsNullOrEmpty(strStart))\n                    start = DateTime.Parse(strStart, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);\n\n                DateTime? end = null;\n                string strEnd = request.QueryOrForm(\"end\");\n                if (!string.IsNullOrEmpty(strEnd))\n                    end = DateTime.Parse(strEnd, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);\n\n                IPAddress clientIpAddress = request.GetQueryOrForm(\"clientIpAddress\", IPAddress.Parse, null);\n\n                DnsTransportProtocol? protocol = null;\n                string strProtocol = request.QueryOrForm(\"protocol\");\n                if (!string.IsNullOrEmpty(strProtocol))\n                    protocol = Enum.Parse<DnsTransportProtocol>(strProtocol, true);\n\n                DnsServerResponseType? responseType = null;\n                string strResponseType = request.QueryOrForm(\"responseType\");\n                if (!string.IsNullOrEmpty(strResponseType))\n                    responseType = Enum.Parse<DnsServerResponseType>(strResponseType, true);\n\n                DnsResponseCode? rcode = null;\n                string strRcode = request.QueryOrForm(\"rcode\");\n                if (!string.IsNullOrEmpty(strRcode))\n                    rcode = Enum.Parse<DnsResponseCode>(strRcode, true);\n\n                string qname = request.GetQueryOrForm(\"qname\", null);\n\n                DnsResourceRecordType? qtype = null;\n                string strQtype = request.QueryOrForm(\"qtype\");\n                if (!string.IsNullOrEmpty(strQtype))\n                    qtype = Enum.Parse<DnsResourceRecordType>(strQtype, true);\n\n                DnsClass? qclass = null;\n                string strQclass = request.QueryOrForm(\"qclass\");\n                if (!string.IsNullOrEmpty(strQclass))\n                    qclass = Enum.Parse<DnsClass>(strQclass, true);\n\n                static async Task WriteCsvFieldAsync(StreamWriter sW, string data)\n                {\n                    if ((data is null) || (data.Length == 0))\n                        return;\n\n                    if (data.Contains('\"', StringComparison.OrdinalIgnoreCase))\n                    {\n                        await sW.WriteAsync('\"');\n                        await sW.WriteAsync(data.Replace(\"\\\"\", \"\\\"\\\"\"));\n                        await sW.WriteAsync('\"');\n                    }\n                    else if (data.Contains(',', StringComparison.OrdinalIgnoreCase) || data.Contains(' ', StringComparison.OrdinalIgnoreCase))\n                    {\n                        await sW.WriteAsync('\"');\n                        await sW.WriteAsync(data);\n                        await sW.WriteAsync('\"');\n                    }\n                    else\n                    {\n                        await sW.WriteAsync(data);\n                    }\n                }\n\n                DnsLogPage page;\n                long pageNumber = 1;\n                string tmpFile = Path.GetTempFileName();\n\n                try\n                {\n                    using (FileStream csvFileStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                    {\n                        StreamWriter sW = new StreamWriter(csvFileStream, Encoding.UTF8);\n\n                        await sW.WriteLineAsync(\"RowNumber,Timestamp,ClientIpAddress,Protocol,ResponseType,ResponseRtt,RCODE,Domain,Type,Class,Answer\");\n\n                        do\n                        {\n                            page = await queryLogs.QueryLogsAsync(pageNumber, 10000, false, start, end, clientIpAddress, protocol, responseType, rcode, qname, qtype, qclass);\n\n                            foreach (DnsLogEntry entry in page.Entries)\n                            {\n                                await WriteCsvFieldAsync(sW, entry.RowNumber.ToString());\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.Timestamp.ToString(\"O\"));\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.ClientIpAddress.ToString());\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.Protocol.ToString());\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.ResponseType.ToString());\n                                await sW.WriteAsync(',');\n\n                                if (entry.ResponseRtt.HasValue)\n                                    await WriteCsvFieldAsync(sW, entry.ResponseRtt.Value.ToString());\n\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.RCODE.ToString());\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.Question?.Name.ToString());\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.Question?.Type.ToString());\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.Question?.Class.ToString());\n                                await sW.WriteAsync(',');\n                                await WriteCsvFieldAsync(sW, entry.Answer);\n\n                                await sW.WriteLineAsync();\n                            }\n                        }\n                        while (pageNumber++ < page.TotalPages);\n\n                        await sW.FlushAsync();\n\n                        //send csv file\n                        csvFileStream.Position = 0;\n\n                        HttpResponse response = context.Response;\n\n                        response.ContentType = \"text/csv\";\n                        response.ContentLength = csvFileStream.Length;\n                        response.Headers.ContentDisposition = \"attachment;filename=\" + _dnsWebService._dnsServer.ServerDomain + DateTime.UtcNow.ToString(\"_yyyy-MM-dd_HH-mm-ss\") + \"_query_logs.csv\";\n\n                        using (Stream output = response.Body)\n                        {\n                            await csvFileStream.CopyToAsync(output);\n                        }\n                    }\n                }\n                finally\n                {\n                    try\n                    {\n                        File.Delete(tmpFile);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsWebService._log.Write(ex);\n                    }\n                }\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceOtherZonesApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Dns.Zones;\nusing Microsoft.AspNetCore.Http;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        class WebServiceOtherZonesApi\n        {\n            #region variables\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceOtherZonesApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region public\n\n            #region cache api\n\n            public void FlushCache(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.CacheZoneManager.Flush();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Cache was flushed.\");\n            }\n\n            public void ListCachedZones(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string domain = request.GetQueryOrForm(\"domain\", \"\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                string direction = request.QueryOrForm(\"direction\");\n                if (direction is not null)\n                    direction = direction.ToLowerInvariant();\n\n                List<string> subZones = new List<string>();\n                List<DnsResourceRecord> records = new List<DnsResourceRecord>();\n\n                while (true)\n                {\n                    subZones.Clear();\n                    records.Clear();\n\n                    _dnsWebService._dnsServer.CacheZoneManager.ListSubDomains(domain, subZones);\n                    _dnsWebService._dnsServer.CacheZoneManager.ListAllRecords(domain, records);\n\n                    if (records.Count > 0)\n                        break;\n\n                    if (subZones.Count != 1)\n                        break;\n\n                    if (direction == \"up\")\n                    {\n                        if (domain.Length == 0)\n                            break;\n\n                        int i = domain.IndexOf('.');\n                        if (i < 0)\n                            domain = \"\";\n                        else\n                            domain = domain.Substring(i + 1);\n                    }\n                    else if (domain.Length == 0)\n                    {\n                        domain = subZones[0];\n                    }\n                    else\n                    {\n                        domain = subZones[0] + \".\" + domain;\n                    }\n                }\n\n                subZones.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"domain\", domain);\n\n                if (DnsClient.TryConvertDomainNameToUnicode(domain, out string idn))\n                    jsonWriter.WriteString(\"domainIdn\", idn);\n\n                jsonWriter.WritePropertyName(\"zones\");\n                jsonWriter.WriteStartArray();\n\n                if (domain.Length != 0)\n                    domain = \".\" + domain;\n\n                foreach (string subZone in subZones)\n                {\n                    string zone = subZone + domain;\n\n                    if (DnsClient.TryConvertDomainNameToUnicode(zone, out string zoneIdn))\n                        zone = zoneIdn;\n\n                    jsonWriter.WriteStringValue(zone);\n                }\n\n                jsonWriter.WriteEndArray();\n\n                WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, false);\n            }\n\n            public void DeleteCachedZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string domain = context.Request.GetQueryOrForm(\"domain\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                if (_dnsWebService._dnsServer.CacheZoneManager.DeleteZone(domain))\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Cached zone was deleted: \" + domain);\n            }\n\n            #endregion\n\n            #region allowed zones api\n\n            public void ListAllowedZones(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string domain = request.GetQueryOrForm(\"domain\", \"\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                string direction = request.QueryOrForm(\"direction\");\n                if (direction is not null)\n                    direction = direction.ToLowerInvariant();\n\n                List<string> subZones = new List<string>();\n                List<DnsResourceRecord> records = new List<DnsResourceRecord>();\n\n                while (true)\n                {\n                    subZones.Clear();\n                    records.Clear();\n\n                    _dnsWebService._dnsServer.AllowedZoneManager.ListSubDomains(domain, subZones);\n                    _dnsWebService._dnsServer.AllowedZoneManager.ListAllRecords(domain, records);\n\n                    if (records.Count > 0)\n                        break;\n\n                    if (subZones.Count != 1)\n                        break;\n\n                    if (direction == \"up\")\n                    {\n                        if (domain.Length == 0)\n                            break;\n\n                        int i = domain.IndexOf('.');\n                        if (i < 0)\n                            domain = \"\";\n                        else\n                            domain = domain.Substring(i + 1);\n                    }\n                    else if (domain.Length == 0)\n                    {\n                        domain = subZones[0];\n                    }\n                    else\n                    {\n                        domain = subZones[0] + \".\" + domain;\n                    }\n                }\n\n                subZones.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"domain\", domain);\n\n                if (DnsClient.TryConvertDomainNameToUnicode(domain, out string idn))\n                    jsonWriter.WriteString(\"domainIdn\", idn);\n\n                jsonWriter.WritePropertyName(\"zones\");\n                jsonWriter.WriteStartArray();\n\n                if (domain.Length != 0)\n                    domain = \".\" + domain;\n\n                foreach (string subZone in subZones)\n                {\n                    string zone = subZone + domain;\n\n                    if (DnsClient.TryConvertDomainNameToUnicode(zone, out string zoneIdn))\n                        zone = zoneIdn;\n\n                    jsonWriter.WriteStringValue(zone);\n                }\n\n                jsonWriter.WriteEndArray();\n\n                WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, true);\n            }\n\n            public void ImportAllowedZones(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string allowedZones = request.GetQueryOrForm(\"allowedZones\");\n                string[] allowedZonesList = allowedZones.Split(',');\n\n                for (int i = 0; i < allowedZonesList.Length; i++)\n                {\n                    if (DnsClient.IsDomainNameUnicode(allowedZonesList[i]))\n                        allowedZonesList[i] = DnsClient.ConvertDomainNameToAscii(allowedZonesList[i]);\n                }\n\n                _dnsWebService._dnsServer.AllowedZoneManager.ImportZones(allowedZonesList);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Total \" + allowedZonesList.Length + \" zones were imported into allowed zone successfully.\");\n                _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public async Task ExportAllowedZonesAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                IReadOnlyList<AuthZoneInfo> zoneInfoList = _dnsWebService._dnsServer.AllowedZoneManager.GetAllZones();\n\n                HttpResponse response = context.Response;\n\n                response.ContentType = \"text/plain\";\n                response.Headers.ContentDisposition = \"attachment;filename=AllowedZones.txt\";\n\n                await using (StreamWriter sW = new StreamWriter(response.Body))\n                {\n                    foreach (AuthZoneInfo zoneInfo in zoneInfoList)\n                        await sW.WriteLineAsync(zoneInfo.Name);\n                }\n            }\n\n            public void DeleteAllowedZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string domain = context.Request.GetQueryOrForm(\"domain\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                if (_dnsWebService._dnsServer.AllowedZoneManager.DeleteZone(domain))\n                {\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Allowed zone was deleted: \" + domain);\n                    _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile();\n\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                        _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n                }\n            }\n\n            public void FlushAllowedZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.AllowedZoneManager.Flush();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Allowed zone was flushed successfully.\");\n                _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void AllowZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string domain = context.Request.GetQueryOrForm(\"domain\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                if (IPAddress.TryParse(domain, out IPAddress ipAddress))\n                    domain = ipAddress.GetReverseDomain();\n\n                if (_dnsWebService._dnsServer.AllowedZoneManager.AllowZone(domain))\n                {\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Zone was allowed: \" + domain);\n                    _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile();\n\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                        _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n                }\n            }\n\n            #endregion\n\n            #region blocked zones api\n\n            public void ListBlockedZones(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string domain = request.GetQueryOrForm(\"domain\", \"\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                string direction = request.QueryOrForm(\"direction\");\n                if (direction is not null)\n                    direction = direction.ToLowerInvariant();\n\n                List<string> subZones = new List<string>();\n                List<DnsResourceRecord> records = new List<DnsResourceRecord>();\n\n                while (true)\n                {\n                    subZones.Clear();\n                    records.Clear();\n\n                    _dnsWebService._dnsServer.BlockedZoneManager.ListSubDomains(domain, subZones);\n                    _dnsWebService._dnsServer.BlockedZoneManager.ListAllRecords(domain, records);\n\n                    if (records.Count > 0)\n                        break;\n\n                    if (subZones.Count != 1)\n                        break;\n\n                    if (direction == \"up\")\n                    {\n                        if (domain.Length == 0)\n                            break;\n\n                        int i = domain.IndexOf('.');\n                        if (i < 0)\n                            domain = \"\";\n                        else\n                            domain = domain.Substring(i + 1);\n                    }\n                    else if (domain.Length == 0)\n                    {\n                        domain = subZones[0];\n                    }\n                    else\n                    {\n                        domain = subZones[0] + \".\" + domain;\n                    }\n                }\n\n                subZones.Sort();\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"domain\", domain);\n\n                if (DnsClient.TryConvertDomainNameToUnicode(domain, out string idn))\n                    jsonWriter.WriteString(\"domainIdn\", idn);\n\n                jsonWriter.WritePropertyName(\"zones\");\n                jsonWriter.WriteStartArray();\n\n                if (domain.Length != 0)\n                    domain = \".\" + domain;\n\n                foreach (string subZone in subZones)\n                {\n                    string zone = subZone + domain;\n\n                    if (DnsClient.TryConvertDomainNameToUnicode(zone, out string zoneIdn))\n                        zone = zoneIdn;\n\n                    jsonWriter.WriteStringValue(zone);\n                }\n\n                jsonWriter.WriteEndArray();\n\n                WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, true);\n            }\n\n            public void ImportBlockedZones(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string blockedZones = request.GetQueryOrForm(\"blockedZones\");\n                string[] blockedZonesList = blockedZones.Split(',');\n\n                for (int i = 0; i < blockedZonesList.Length; i++)\n                {\n                    if (DnsClient.IsDomainNameUnicode(blockedZonesList[i]))\n                        blockedZonesList[i] = DnsClient.ConvertDomainNameToAscii(blockedZonesList[i]);\n                }\n\n                _dnsWebService._dnsServer.BlockedZoneManager.ImportZones(blockedZonesList);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Total \" + blockedZonesList.Length + \" zones were imported into blocked zone successfully.\");\n                _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public async Task ExportBlockedZonesAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                IReadOnlyList<AuthZoneInfo> zoneInfoList = _dnsWebService._dnsServer.BlockedZoneManager.GetAllZones();\n\n                HttpResponse response = context.Response;\n\n                response.ContentType = \"text/plain\";\n                response.Headers.ContentDisposition = \"attachment;filename=BlockedZones.txt\";\n\n                await using (StreamWriter sW = new StreamWriter(response.Body))\n                {\n                    foreach (AuthZoneInfo zoneInfo in zoneInfoList)\n                        await sW.WriteLineAsync(zoneInfo.Name);\n                }\n            }\n\n            public void DeleteBlockedZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string domain = context.Request.GetQueryOrForm(\"domain\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                if (_dnsWebService._dnsServer.BlockedZoneManager.DeleteZone(domain))\n                {\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Blocked zone was deleted: \" + domain);\n                    _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile();\n\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                        _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n                }\n            }\n\n            public void FlushBlockedZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.BlockedZoneManager.Flush();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Blocked zone was flushed successfully.\");\n                _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile();\n\n                //trigger cluster update\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                    _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n            }\n\n            public void BlockZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string domain = context.Request.GetQueryOrForm(\"domain\");\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                if (IPAddress.TryParse(domain, out IPAddress ipAddress))\n                    domain = ipAddress.GetReverseDomain();\n\n                if (_dnsWebService._dnsServer.BlockedZoneManager.BlockZone(domain))\n                {\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Domain was added to blocked zone: \" + domain);\n                    _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile();\n\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                        _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n                }\n            }\n\n            #endregion\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceSettingsApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Cluster;\nusing DnsServerCore.Dns;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.Immutable;\nusing System.IO;\nusing System.Net;\nusing System.Net.Mail;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ClientConnection;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        sealed class WebServiceSettingsApi\n        {\n            #region variables\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceSettingsApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region private\n\n            private void RestartService(bool restartDnsService, bool restartWebService, IReadOnlyList<IPAddress> oldWebServiceLocalAddresses, int oldWebServiceHttpPort, int oldWebServiceTlsPort)\n            {\n                if (restartDnsService)\n                {\n                    ThreadPool.QueueUserWorkItem(async delegate (object state)\n                    {\n                        try\n                        {\n                            _dnsWebService._log.Write(\"Attempting to restart DNS service.\");\n\n                            await _dnsWebService._dnsServer.StopAsync();\n                            await _dnsWebService._dnsServer.StartAsync();\n\n                            _dnsWebService._log.Write(\"DNS service was restarted successfully.\");\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(\"Failed to restart DNS service.\\r\\n\" + ex.ToString());\n                        }\n                    });\n                }\n\n                if (restartWebService)\n                {\n                    ThreadPool.QueueUserWorkItem(async delegate (object state)\n                    {\n                        try\n                        {\n                            await Task.Delay(2000); //wait for this HTTP response to be delivered before stopping web server\n\n                            _dnsWebService._log.Write(\"Attempting to restart web service.\");\n\n                            try\n                            {\n                                await _dnsWebService.StopWebServiceAsync();\n                                await _dnsWebService.TryStartWebServiceAsync(oldWebServiceLocalAddresses, oldWebServiceHttpPort, oldWebServiceTlsPort);\n\n                                _dnsWebService._log.Write(\"Web service was restarted successfully.\");\n                            }\n                            catch (Exception ex)\n                            {\n                                _dnsWebService._log.Write(\"Failed to restart web service.\\r\\n\" + ex.ToString());\n                            }\n\n                            //update cluster node URL to reflect latest TLS port\n                            if (_dnsWebService._clusterManager.ClusterInitialized)\n                                _dnsWebService._clusterManager.UpdateSelfNodeUrlAndCertificate();\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(ex);\n                        }\n                    });\n                }\n            }\n\n            private void WriteDnsSettings(Utf8JsonWriter jsonWriter)\n            {\n                //info\n                jsonWriter.WriteString(\"version\", _dnsWebService.GetServerVersion());\n                jsonWriter.WriteString(\"uptimestamp\", _dnsWebService._uptimestamp);\n\n                jsonWriter.WriteBoolean(\"clusterInitialized\", _dnsWebService._clusterManager.ClusterInitialized);\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    jsonWriter.WriteString(\"clusterDomain\", _dnsWebService._clusterManager.ClusterDomain);\n\n                    _dnsWebService._clusterApi.WriteClusterNodes(jsonWriter);\n                }\n\n                //general\n                jsonWriter.WriteString(\"dnsServerDomain\", _dnsWebService._dnsServer.ServerDomain);\n\n                jsonWriter.WriteStringArray(\"dnsServerLocalEndPoints\", _dnsWebService._dnsServer.LocalEndPoints);\n\n                jsonWriter.WriteStringArray(\"dnsServerIPv4SourceAddresses\", DnsClientConnection.IPv4SourceAddresses);\n                jsonWriter.WriteStringArray(\"dnsServerIPv6SourceAddresses\", DnsClientConnection.IPv6SourceAddresses);\n\n                jsonWriter.WriteNumber(\"defaultRecordTtl\", _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl);\n                jsonWriter.WriteNumber(\"defaultNsRecordTtl\", _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl);\n                jsonWriter.WriteNumber(\"defaultSoaRecordTtl\", _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl);\n                jsonWriter.WriteString(\"defaultResponsiblePerson\", _dnsWebService._dnsServer.DefaultResponsiblePerson?.Address);\n                jsonWriter.WriteBoolean(\"useSoaSerialDateScheme\", _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme);\n                jsonWriter.WriteNumber(\"minSoaRefresh\", _dnsWebService._dnsServer.AuthZoneManager.MinSoaRefresh);\n                jsonWriter.WriteNumber(\"minSoaRetry\", _dnsWebService._dnsServer.AuthZoneManager.MinSoaRetry);\n                jsonWriter.WriteStringArray(\"zoneTransferAllowedNetworks\", _dnsWebService._dnsServer.ZoneTransferAllowedNetworks);\n                jsonWriter.WriteStringArray(\"notifyAllowedNetworks\", _dnsWebService._dnsServer.NotifyAllowedNetworks);\n\n                jsonWriter.WriteBoolean(\"dnsAppsEnableAutomaticUpdate\", _dnsWebService._dnsServer.DnsApplicationManager.EnableAutomaticUpdate);\n\n                jsonWriter.WriteBoolean(\"preferIPv6\", _dnsWebService._dnsServer.PreferIPv6);\n                jsonWriter.WriteBoolean(\"enableUdpSocketPool\", _dnsWebService._dnsServer.EnableUdpSocketPool);\n\n                jsonWriter.WriteStartArray(\"socketPoolExcludedPorts\");\n\n                ushort[] socketPoolExcludedPorts = UdpClientConnection.SocketPoolExcludedPorts;\n                if (socketPoolExcludedPorts is not null)\n                {\n                    foreach (ushort excludedPort in socketPoolExcludedPorts)\n                        jsonWriter.WriteNumberValue(excludedPort);\n                }\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WriteNumber(\"udpPayloadSize\", _dnsWebService._dnsServer.UdpPayloadSize);\n\n                jsonWriter.WriteBoolean(\"dnssecValidation\", _dnsWebService._dnsServer.DnssecValidation);\n\n                jsonWriter.WriteBoolean(\"eDnsClientSubnet\", _dnsWebService._dnsServer.EDnsClientSubnet);\n                jsonWriter.WriteNumber(\"eDnsClientSubnetIPv4PrefixLength\", _dnsWebService._dnsServer.EDnsClientSubnetIPv4PrefixLength);\n                jsonWriter.WriteNumber(\"eDnsClientSubnetIPv6PrefixLength\", _dnsWebService._dnsServer.EDnsClientSubnetIPv6PrefixLength);\n                jsonWriter.WriteString(\"eDnsClientSubnetIpv4Override\", _dnsWebService._dnsServer.EDnsClientSubnetIpv4Override?.ToString());\n                jsonWriter.WriteString(\"eDnsClientSubnetIpv6Override\", _dnsWebService._dnsServer.EDnsClientSubnetIpv6Override?.ToString());\n\n                jsonWriter.WriteStartArray(\"qpmPrefixLimitsIPv4\");\n\n                foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in _dnsWebService._dnsServer.QpmPrefixLimitsIPv4)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteNumber(\"prefix\", qpmPrefixLimit.Key);\n                    jsonWriter.WriteNumber(\"udpLimit\", qpmPrefixLimit.Value.Item1);\n                    jsonWriter.WriteNumber(\"tcpLimit\", qpmPrefixLimit.Value.Item2);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WriteStartArray(\"qpmPrefixLimitsIPv6\");\n\n                foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in _dnsWebService._dnsServer.QpmPrefixLimitsIPv6)\n                {\n                    jsonWriter.WriteStartObject();\n\n                    jsonWriter.WriteNumber(\"prefix\", qpmPrefixLimit.Key);\n                    jsonWriter.WriteNumber(\"udpLimit\", qpmPrefixLimit.Value.Item1);\n                    jsonWriter.WriteNumber(\"tcpLimit\", qpmPrefixLimit.Value.Item2);\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WriteNumber(\"qpmLimitSampleMinutes\", _dnsWebService._dnsServer.QpmLimitSampleMinutes);\n                jsonWriter.WriteNumber(\"qpmLimitUdpTruncationPercentage\", _dnsWebService._dnsServer.QpmLimitUdpTruncationPercentage);\n\n                jsonWriter.WritePropertyName(\"qpmLimitBypassList\");\n                jsonWriter.WriteStartArray();\n\n                if (_dnsWebService._dnsServer.QpmLimitBypassList is not null)\n                {\n                    foreach (NetworkAddress network in _dnsWebService._dnsServer.QpmLimitBypassList)\n                        jsonWriter.WriteStringValue(network.ToString());\n                }\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WriteNumber(\"clientTimeout\", _dnsWebService._dnsServer.ClientTimeout);\n                jsonWriter.WriteNumber(\"tcpSendTimeout\", _dnsWebService._dnsServer.TcpSendTimeout);\n                jsonWriter.WriteNumber(\"tcpReceiveTimeout\", _dnsWebService._dnsServer.TcpReceiveTimeout);\n                jsonWriter.WriteNumber(\"quicIdleTimeout\", _dnsWebService._dnsServer.QuicIdleTimeout);\n                jsonWriter.WriteNumber(\"quicMaxInboundStreams\", _dnsWebService._dnsServer.QuicMaxInboundStreams);\n                jsonWriter.WriteNumber(\"listenBacklog\", _dnsWebService._dnsServer.ListenBacklog);\n                jsonWriter.WriteNumber(\"maxConcurrentResolutionsPerCore\", _dnsWebService._dnsServer.MaxConcurrentResolutionsPerCore);\n\n                //web service\n                jsonWriter.WritePropertyName(\"webServiceLocalAddresses\");\n                jsonWriter.WriteStartArray();\n\n                foreach (IPAddress localAddress in _dnsWebService._webServiceLocalAddresses)\n                {\n                    if (localAddress.AddressFamily == AddressFamily.InterNetworkV6)\n                        jsonWriter.WriteStringValue(\"[\" + localAddress.ToString() + \"]\");\n                    else\n                        jsonWriter.WriteStringValue(localAddress.ToString());\n                }\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WriteNumber(\"webServiceHttpPort\", _dnsWebService._webServiceHttpPort);\n                jsonWriter.WriteBoolean(\"webServiceEnableTls\", _dnsWebService._webServiceEnableTls);\n                jsonWriter.WriteBoolean(\"webServiceEnableHttp3\", _dnsWebService._webServiceEnableHttp3);\n                jsonWriter.WriteBoolean(\"webServiceHttpToTlsRedirect\", _dnsWebService._webServiceHttpToTlsRedirect);\n                jsonWriter.WriteBoolean(\"webServiceUseSelfSignedTlsCertificate\", _dnsWebService._webServiceUseSelfSignedTlsCertificate);\n                jsonWriter.WriteNumber(\"webServiceTlsPort\", _dnsWebService._webServiceTlsPort);\n                jsonWriter.WriteString(\"webServiceTlsCertificatePath\", _dnsWebService._webServiceTlsCertificatePath);\n                jsonWriter.WriteString(\"webServiceTlsCertificatePassword\", \"************\");\n                jsonWriter.WriteString(\"webServiceRealIpHeader\", _dnsWebService._webServiceRealIpHeader);\n\n                //optional protocols\n                jsonWriter.WriteBoolean(\"enableDnsOverUdpProxy\", _dnsWebService._dnsServer.EnableDnsOverUdpProxy);\n                jsonWriter.WriteBoolean(\"enableDnsOverTcpProxy\", _dnsWebService._dnsServer.EnableDnsOverTcpProxy);\n                jsonWriter.WriteBoolean(\"enableDnsOverHttp\", _dnsWebService._dnsServer.EnableDnsOverHttp);\n                jsonWriter.WriteBoolean(\"enableDnsOverTls\", _dnsWebService._dnsServer.EnableDnsOverTls);\n                jsonWriter.WriteBoolean(\"enableDnsOverHttps\", _dnsWebService._dnsServer.EnableDnsOverHttps);\n                jsonWriter.WriteBoolean(\"enableDnsOverHttp3\", _dnsWebService._dnsServer.EnableDnsOverHttp3);\n                jsonWriter.WriteBoolean(\"enableDnsOverQuic\", _dnsWebService._dnsServer.EnableDnsOverQuic);\n                jsonWriter.WriteNumber(\"dnsOverUdpProxyPort\", _dnsWebService._dnsServer.DnsOverUdpProxyPort);\n                jsonWriter.WriteNumber(\"dnsOverTcpProxyPort\", _dnsWebService._dnsServer.DnsOverTcpProxyPort);\n                jsonWriter.WriteNumber(\"dnsOverHttpPort\", _dnsWebService._dnsServer.DnsOverHttpPort);\n                jsonWriter.WriteNumber(\"dnsOverTlsPort\", _dnsWebService._dnsServer.DnsOverTlsPort);\n                jsonWriter.WriteNumber(\"dnsOverHttpsPort\", _dnsWebService._dnsServer.DnsOverHttpsPort);\n                jsonWriter.WriteNumber(\"dnsOverQuicPort\", _dnsWebService._dnsServer.DnsOverQuicPort);\n\n                jsonWriter.WritePropertyName(\"reverseProxyNetworkACL\");\n                {\n                    jsonWriter.WriteStartArray();\n\n                    if (_dnsWebService._dnsServer.ReverseProxyNetworkACL is not null)\n                    {\n                        foreach (NetworkAccessControl nac in _dnsWebService._dnsServer.ReverseProxyNetworkACL)\n                            jsonWriter.WriteStringValue(nac.ToString());\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                jsonWriter.WriteString(\"dnsTlsCertificatePath\", _dnsWebService._dnsServer.DnsTlsCertificatePath);\n                jsonWriter.WriteString(\"dnsTlsCertificatePassword\", \"************\");\n                jsonWriter.WriteString(\"dnsOverHttpRealIpHeader\", _dnsWebService._dnsServer.DnsOverHttpRealIpHeader);\n\n                //tsig\n                jsonWriter.WritePropertyName(\"tsigKeys\");\n                {\n                    jsonWriter.WriteStartArray();\n\n                    if (_dnsWebService._dnsServer.TsigKeys is not null)\n                    {\n                        foreach (KeyValuePair<string, TsigKey> tsigKey in _dnsWebService._dnsServer.TsigKeys.ToImmutableSortedDictionary())\n                        {\n                            jsonWriter.WriteStartObject();\n\n                            jsonWriter.WriteString(\"keyName\", tsigKey.Key);\n                            jsonWriter.WriteString(\"sharedSecret\", tsigKey.Value.SharedSecret);\n                            jsonWriter.WriteString(\"algorithmName\", tsigKey.Value.AlgorithmName);\n\n                            jsonWriter.WriteEndObject();\n                        }\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                //recursion\n                jsonWriter.WriteString(\"recursion\", _dnsWebService._dnsServer.Recursion.ToString());\n\n                jsonWriter.WritePropertyName(\"recursionNetworkACL\");\n                {\n                    jsonWriter.WriteStartArray();\n\n                    if (_dnsWebService._dnsServer.RecursionNetworkACL is not null)\n                    {\n                        foreach (NetworkAccessControl nac in _dnsWebService._dnsServer.RecursionNetworkACL)\n                            jsonWriter.WriteStringValue(nac.ToString());\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                jsonWriter.WriteBoolean(\"randomizeName\", _dnsWebService._dnsServer.RandomizeName);\n                jsonWriter.WriteBoolean(\"qnameMinimization\", _dnsWebService._dnsServer.QnameMinimization);\n\n                jsonWriter.WriteNumber(\"resolverRetries\", _dnsWebService._dnsServer.ResolverRetries);\n                jsonWriter.WriteNumber(\"resolverTimeout\", _dnsWebService._dnsServer.ResolverTimeout);\n                jsonWriter.WriteNumber(\"resolverConcurrency\", _dnsWebService._dnsServer.ResolverConcurrency);\n                jsonWriter.WriteNumber(\"resolverMaxStackCount\", _dnsWebService._dnsServer.ResolverMaxStackCount);\n\n                //cache\n                jsonWriter.WriteBoolean(\"saveCache\", _dnsWebService._dnsServer.SaveCacheToDisk);\n                jsonWriter.WriteBoolean(\"serveStale\", _dnsWebService._dnsServer.ServeStale);\n                jsonWriter.WriteNumber(\"serveStaleTtl\", _dnsWebService._dnsServer.CacheZoneManager.ServeStaleTtl);\n                jsonWriter.WriteNumber(\"serveStaleAnswerTtl\", _dnsWebService._dnsServer.CacheZoneManager.ServeStaleAnswerTtl);\n                jsonWriter.WriteNumber(\"serveStaleResetTtl\", _dnsWebService._dnsServer.CacheZoneManager.ServeStaleResetTtl);\n                jsonWriter.WriteNumber(\"serveStaleMaxWaitTime\", _dnsWebService._dnsServer.ServeStaleMaxWaitTime);\n\n                jsonWriter.WriteNumber(\"cacheMaximumEntries\", _dnsWebService._dnsServer.CacheZoneManager.MaximumEntries);\n                jsonWriter.WriteNumber(\"cacheMinimumRecordTtl\", _dnsWebService._dnsServer.CacheZoneManager.MinimumRecordTtl);\n                jsonWriter.WriteNumber(\"cacheMaximumRecordTtl\", _dnsWebService._dnsServer.CacheZoneManager.MaximumRecordTtl);\n                jsonWriter.WriteNumber(\"cacheNegativeRecordTtl\", _dnsWebService._dnsServer.CacheZoneManager.NegativeRecordTtl);\n                jsonWriter.WriteNumber(\"cacheFailureRecordTtl\", _dnsWebService._dnsServer.CacheZoneManager.FailureRecordTtl);\n\n                jsonWriter.WriteNumber(\"cachePrefetchEligibility\", _dnsWebService._dnsServer.CachePrefetchEligibility);\n                jsonWriter.WriteNumber(\"cachePrefetchTrigger\", _dnsWebService._dnsServer.CachePrefetchTrigger);\n                jsonWriter.WriteNumber(\"cachePrefetchSampleIntervalInMinutes\", _dnsWebService._dnsServer.CachePrefetchSampleIntervalMinutes);\n                jsonWriter.WriteNumber(\"cachePrefetchSampleEligibilityHitsPerHour\", _dnsWebService._dnsServer.CachePrefetchSampleEligibilityHitsPerHour);\n\n                //blocking\n                jsonWriter.WriteBoolean(\"enableBlocking\", _dnsWebService._dnsServer.EnableBlocking);\n                jsonWriter.WriteBoolean(\"allowTxtBlockingReport\", _dnsWebService._dnsServer.AllowTxtBlockingReport);\n\n                jsonWriter.WritePropertyName(\"blockingBypassList\");\n                jsonWriter.WriteStartArray();\n\n                if (_dnsWebService._dnsServer.BlockingBypassList is not null)\n                {\n                    foreach (NetworkAddress network in _dnsWebService._dnsServer.BlockingBypassList)\n                        jsonWriter.WriteStringValue(network.ToString());\n                }\n\n                jsonWriter.WriteEndArray();\n\n                if (!_dnsWebService._dnsServer.EnableBlocking && (DateTime.UtcNow < _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlockingTill))\n                    jsonWriter.WriteString(\"temporaryDisableBlockingTill\", _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlockingTill);\n\n                jsonWriter.WriteString(\"blockingType\", _dnsWebService._dnsServer.BlockingType.ToString());\n                jsonWriter.WriteNumber(\"blockingAnswerTtl\", _dnsWebService._dnsServer.BlockingAnswerTtl);\n\n                jsonWriter.WritePropertyName(\"customBlockingAddresses\");\n                jsonWriter.WriteStartArray();\n\n                foreach (DnsARecordData record in _dnsWebService._dnsServer.CustomBlockingARecords)\n                    jsonWriter.WriteStringValue(record.Address.ToString());\n\n                foreach (DnsAAAARecordData record in _dnsWebService._dnsServer.CustomBlockingAAAARecords)\n                    jsonWriter.WriteStringValue(record.Address.ToString());\n\n                jsonWriter.WriteEndArray();\n\n                jsonWriter.WritePropertyName(\"blockListUrls\");\n\n                if (_dnsWebService._dnsServer.BlockListZoneManager.BlockListUrls.Count == 0)\n                {\n                    jsonWriter.WriteNullValue();\n                }\n                else\n                {\n                    jsonWriter.WriteStartArray();\n\n                    foreach (string blockListUrl in _dnsWebService._dnsServer.BlockListZoneManager.BlockListUrls)\n                        jsonWriter.WriteStringValue(blockListUrl);\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                jsonWriter.WriteNumber(\"blockListUpdateIntervalHours\", _dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours);\n\n                if (_dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateEnabled)\n                {\n                    DateTime blockListNextUpdatedOn = _dnsWebService._dnsServer.BlockListZoneManager.BlockListLastUpdatedOn.AddHours(_dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours);\n\n                    jsonWriter.WriteString(\"blockListNextUpdatedOn\", blockListNextUpdatedOn);\n                }\n\n                //proxy & forwarders\n                jsonWriter.WritePropertyName(\"proxy\");\n                if (_dnsWebService._dnsServer.Proxy == null)\n                {\n                    jsonWriter.WriteNullValue();\n                }\n                else\n                {\n                    jsonWriter.WriteStartObject();\n\n                    NetProxy proxy = _dnsWebService._dnsServer.Proxy;\n\n                    jsonWriter.WriteString(\"type\", proxy.Type.ToString());\n                    jsonWriter.WriteString(\"address\", proxy.Address);\n                    jsonWriter.WriteNumber(\"port\", proxy.Port);\n\n                    NetworkCredential credential = proxy.Credential;\n                    if (credential != null)\n                    {\n                        jsonWriter.WriteString(\"username\", credential.UserName);\n                        jsonWriter.WriteString(\"password\", credential.Password);\n                    }\n\n                    jsonWriter.WritePropertyName(\"bypass\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (NetProxyBypassItem item in proxy.BypassList)\n                        jsonWriter.WriteStringValue(item.Value);\n\n                    jsonWriter.WriteEndArray();\n\n                    jsonWriter.WriteEndObject();\n                }\n\n                jsonWriter.WritePropertyName(\"forwarders\");\n\n                DnsTransportProtocol forwarderProtocol = DnsTransportProtocol.Udp;\n\n                if (_dnsWebService._dnsServer.Forwarders == null)\n                {\n                    jsonWriter.WriteNullValue();\n                }\n                else\n                {\n                    forwarderProtocol = _dnsWebService._dnsServer.Forwarders[0].Protocol;\n\n                    jsonWriter.WriteStartArray();\n\n                    foreach (NameServerAddress forwarder in _dnsWebService._dnsServer.Forwarders)\n                        jsonWriter.WriteStringValue(forwarder.OriginalAddress);\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                jsonWriter.WriteString(\"forwarderProtocol\", forwarderProtocol.ToString());\n                jsonWriter.WriteBoolean(\"concurrentForwarding\", _dnsWebService._dnsServer.ConcurrentForwarding);\n\n                jsonWriter.WriteNumber(\"forwarderRetries\", _dnsWebService._dnsServer.ForwarderRetries);\n                jsonWriter.WriteNumber(\"forwarderTimeout\", _dnsWebService._dnsServer.ForwarderTimeout);\n                jsonWriter.WriteNumber(\"forwarderConcurrency\", _dnsWebService._dnsServer.ForwarderConcurrency);\n\n                //logging\n                jsonWriter.WriteBoolean(\"enableLogging\", _dnsWebService._log.LoggingType != LoggingType.None);\n                jsonWriter.WriteString(\"loggingType\", _dnsWebService._log.LoggingType.ToString());\n                jsonWriter.WriteBoolean(\"ignoreResolverLogs\", _dnsWebService._dnsServer.ResolverLogManager == null);\n                jsonWriter.WriteBoolean(\"logQueries\", _dnsWebService._dnsServer.QueryLogManager != null);\n                jsonWriter.WriteBoolean(\"useLocalTime\", _dnsWebService._log.UseLocalTime);\n                jsonWriter.WriteString(\"logFolder\", _dnsWebService._log.LogFolder);\n                jsonWriter.WriteNumber(\"maxLogFileDays\", _dnsWebService._log.MaxLogFileDays);\n\n                jsonWriter.WriteBoolean(\"enableInMemoryStats\", _dnsWebService._dnsServer.StatsManager.EnableInMemoryStats);\n                jsonWriter.WriteNumber(\"maxStatFileDays\", _dnsWebService._dnsServer.StatsManager.MaxStatFileDays);\n            }\n\n            #endregion\n\n            #region public\n\n            public void GetDnsSettings(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                WriteDnsSettings(jsonWriter);\n            }\n\n            public async Task SetDnsSettingsAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                bool serverDomainChanged = false;\n                bool webServiceLocalAddressesChanged = false;\n                bool webServiceTlsCertificateChanged = false;\n                bool restartDnsService = false;\n                bool restartWebService = false;\n                IReadOnlyList<IPAddress> oldWebServiceLocalAddresses = _dnsWebService._webServiceLocalAddresses;\n                int oldWebServiceHttpPort = _dnsWebService._webServiceHttpPort;\n                int oldWebServiceTlsPort = _dnsWebService._webServiceTlsPort;\n                bool _webServiceEnablingTls = false;\n\n                Dictionary<string, string> clusterParameters = new Dictionary<string, string>(128);\n\n                HttpRequest request = context.Request;\n                JsonDocument jsonDocument = null;\n\n                if (request.HasJsonContentType())\n                {\n                    jsonDocument = await JsonDocument.ParseAsync(request.Body);\n                    context.Items[\"jsonContent\"] = jsonDocument;\n                }\n\n                try\n                {\n                    try\n                    {\n                        #region general\n\n                        if (request.TryGetQueryOrForm(\"dnsServerDomain\", out string dnsServerDomain))\n                        {\n                            dnsServerDomain = dnsServerDomain.TrimEnd('.');\n\n                            if (!_dnsWebService._dnsServer.ServerDomain.Equals(dnsServerDomain, StringComparison.OrdinalIgnoreCase))\n                            {\n                                if (_dnsWebService._clusterManager.ClusterInitialized)\n                                {\n                                    if (!dnsServerDomain.EndsWith(\".\" + _dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))\n                                        throw new ArgumentException(\"DNS server domain name must end with the cluster domain name.\", nameof(dnsServerDomain));\n                                }\n\n                                _dnsWebService._dnsServer.ServerDomain = dnsServerDomain;\n                                serverDomainChanged = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"dnsServerLocalEndPoints\", IPEndPoint.Parse, out IPEndPoint[] dnsServerLocalEndPoints))\n                        {\n                            if (dnsServerLocalEndPoints.Length == 0)\n                            {\n                                dnsServerLocalEndPoints = [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)];\n                            }\n                            else\n                            {\n                                foreach (IPEndPoint localEndPoint in dnsServerLocalEndPoints)\n                                {\n                                    if (localEndPoint.Port == 0)\n                                        localEndPoint.Port = 53;\n                                }\n                            }\n\n                            if (!_dnsWebService._dnsServer.LocalEndPoints.HasSameItems(dnsServerLocalEndPoints))\n                                restartDnsService = true;\n\n                            _dnsWebService._dnsServer.LocalEndPoints = dnsServerLocalEndPoints;\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"dnsServerIPv4SourceAddresses\", NetworkAddress.Parse, out NetworkAddress[] dnsServerIPv4SourceAddresses))\n                            DnsClientConnection.IPv4SourceAddresses = dnsServerIPv4SourceAddresses;\n\n                        if (request.TryGetQueryOrFormArray(\"dnsServerIPv6SourceAddresses\", NetworkAddress.Parse, out NetworkAddress[] dnsServerIPv6SourceAddresses))\n                            DnsClientConnection.IPv6SourceAddresses = dnsServerIPv6SourceAddresses;\n\n                        if (request.TryGetQueryOrForm(\"defaultRecordTtl\", ZoneFile.ParseTtl, out uint defaultRecordTtl))\n                        {\n                            _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl = defaultRecordTtl;\n\n                            clusterParameters.Add(\"defaultRecordTtl\", defaultRecordTtl.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"defaultNsRecordTtl\", ZoneFile.ParseTtl, out uint defaultNsRecordTtl))\n                        {\n                            _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl = defaultNsRecordTtl;\n\n                            clusterParameters.Add(\"defaultNsRecordTtl\", defaultNsRecordTtl.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"defaultSoaRecordTtl\", ZoneFile.ParseTtl, out uint defaultSoaRecordTtl))\n                        {\n                            _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl = defaultSoaRecordTtl;\n\n                            clusterParameters.Add(\"defaultSoaRecordTtl\", defaultSoaRecordTtl.ToString());\n                        }\n\n                        string defaultResponsiblePerson = request.QueryOrForm(\"defaultResponsiblePerson\");\n                        if (defaultResponsiblePerson is not null)\n                        {\n                            if (defaultResponsiblePerson.Length == 0)\n                                _dnsWebService._dnsServer.DefaultResponsiblePerson = null;\n                            else if (defaultResponsiblePerson.Length > 255)\n                                throw new ArgumentException(\"Default responsible person email address length cannot exceed 255 characters.\", nameof(defaultResponsiblePerson));\n                            else\n                                _dnsWebService._dnsServer.DefaultResponsiblePerson = new MailAddress(defaultResponsiblePerson);\n\n                            clusterParameters.Add(\"defaultResponsiblePerson\", defaultResponsiblePerson);\n                        }\n\n                        if (request.TryGetQueryOrForm(\"useSoaSerialDateScheme\", bool.Parse, out bool useSoaSerialDateScheme))\n                        {\n                            _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme = useSoaSerialDateScheme;\n\n                            clusterParameters.Add(\"useSoaSerialDateScheme\", useSoaSerialDateScheme.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"minSoaRefresh\", ZoneFile.ParseTtl, out uint minSoaRefresh))\n                        {\n                            _dnsWebService._dnsServer.AuthZoneManager.MinSoaRefresh = minSoaRefresh;\n\n                            clusterParameters.Add(\"minSoaRefresh\", minSoaRefresh.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"minSoaRetry\", ZoneFile.ParseTtl, out uint minSoaRetry))\n                        {\n                            _dnsWebService._dnsServer.AuthZoneManager.MinSoaRetry = minSoaRetry;\n\n                            clusterParameters.Add(\"minSoaRetry\", minSoaRetry.ToString());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"zoneTransferAllowedNetworks\", NetworkAddress.Parse, out NetworkAddress[] zoneTransferAllowedNetworks))\n                        {\n                            _dnsWebService._dnsServer.ZoneTransferAllowedNetworks = zoneTransferAllowedNetworks;\n\n                            clusterParameters.Add(\"zoneTransferAllowedNetworks\", zoneTransferAllowedNetworks.Join());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"notifyAllowedNetworks\", NetworkAddress.Parse, out NetworkAddress[] notifyAllowedNetworks))\n                        {\n                            _dnsWebService._dnsServer.NotifyAllowedNetworks = notifyAllowedNetworks;\n\n                            clusterParameters.Add(\"notifyAllowedNetworks\", notifyAllowedNetworks.Join());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsAppsEnableAutomaticUpdate\", bool.Parse, out bool dnsAppsEnableAutomaticUpdate))\n                        {\n                            _dnsWebService._dnsServer.DnsApplicationManager.EnableAutomaticUpdate = dnsAppsEnableAutomaticUpdate;\n\n                            clusterParameters.Add(\"dnsAppsEnableAutomaticUpdate\", dnsAppsEnableAutomaticUpdate.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"preferIPv6\", bool.Parse, out bool preferIPv6))\n                            _dnsWebService._dnsServer.PreferIPv6 = preferIPv6;\n\n                        if (request.TryGetQueryOrForm(\"enableUdpSocketPool\", bool.Parse, out bool enableUdpSocketPool))\n                            _dnsWebService._dnsServer.EnableUdpSocketPool = enableUdpSocketPool;\n\n                        if (request.TryGetQueryOrFormArray(\"socketPoolExcludedPorts\", ushort.Parse, out ushort[] socketPoolExcludedPorts))\n                            UdpClientConnection.SocketPoolExcludedPorts = socketPoolExcludedPorts;\n\n                        if (request.TryGetQueryOrForm(\"udpPayloadSize\", ushort.Parse, out ushort udpPayloadSize))\n                        {\n                            _dnsWebService._dnsServer.UdpPayloadSize = udpPayloadSize;\n\n                            clusterParameters.Add(\"udpPayloadSize\", udpPayloadSize.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnssecValidation\", bool.Parse, out bool dnssecValidation))\n                        {\n                            _dnsWebService._dnsServer.DnssecValidation = dnssecValidation;\n\n                            clusterParameters.Add(\"dnssecValidation\", dnssecValidation.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"eDnsClientSubnet\", bool.Parse, out bool eDnsClientSubnet))\n                        {\n                            _dnsWebService._dnsServer.EDnsClientSubnet = eDnsClientSubnet;\n\n                            clusterParameters.Add(\"eDnsClientSubnet\", eDnsClientSubnet.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"eDnsClientSubnetIPv4PrefixLength\", byte.Parse, out byte eDnsClientSubnetIPv4PrefixLength))\n                        {\n                            _dnsWebService._dnsServer.EDnsClientSubnetIPv4PrefixLength = eDnsClientSubnetIPv4PrefixLength;\n\n                            clusterParameters.Add(\"eDnsClientSubnetIPv4PrefixLength\", eDnsClientSubnetIPv4PrefixLength.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"eDnsClientSubnetIPv6PrefixLength\", byte.Parse, out byte eDnsClientSubnetIPv6PrefixLength))\n                        {\n                            _dnsWebService._dnsServer.EDnsClientSubnetIPv6PrefixLength = eDnsClientSubnetIPv6PrefixLength;\n\n                            clusterParameters.Add(\"eDnsClientSubnetIPv6PrefixLength\", eDnsClientSubnetIPv6PrefixLength.ToString());\n                        }\n\n                        string eDnsClientSubnetIpv4Override = request.QueryOrForm(\"eDnsClientSubnetIpv4Override\");\n                        if (eDnsClientSubnetIpv4Override is not null)\n                        {\n                            if (eDnsClientSubnetIpv4Override.Length == 0)\n                                _dnsWebService._dnsServer.EDnsClientSubnetIpv4Override = null;\n                            else\n                                _dnsWebService._dnsServer.EDnsClientSubnetIpv4Override = NetworkAddress.Parse(eDnsClientSubnetIpv4Override);\n\n                            clusterParameters.Add(\"eDnsClientSubnetIpv4Override\", eDnsClientSubnetIpv4Override);\n                        }\n\n                        string eDnsClientSubnetIpv6Override = request.QueryOrForm(\"eDnsClientSubnetIpv6Override\");\n                        if (eDnsClientSubnetIpv6Override is not null)\n                        {\n                            if (eDnsClientSubnetIpv6Override.Length == 0)\n                                _dnsWebService._dnsServer.EDnsClientSubnetIpv6Override = null;\n                            else\n                                _dnsWebService._dnsServer.EDnsClientSubnetIpv6Override = NetworkAddress.Parse(eDnsClientSubnetIpv6Override);\n\n                            clusterParameters.Add(\"eDnsClientSubnetIpv6Override\", eDnsClientSubnetIpv6Override);\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"qpmPrefixLimitsIPv4\", delegate (JsonElement jsonObject)\n                            {\n                                int prefix = jsonObject.GetProperty(\"prefix\").GetInt32();\n                                int udpLimit = jsonObject.GetProperty(\"udpLimit\").GetInt32();\n                                int tcpLimit = jsonObject.GetProperty(\"tcpLimit\").GetInt32();\n\n                                return new KeyValuePair<int, (int, int)>(prefix, (udpLimit, tcpLimit));\n                            }, delegate (ArraySegment<string> tableRow)\n                            {\n                                int prefix = int.Parse(tableRow[0]);\n                                int udpLimit = int.Parse(tableRow[1]);\n                                int tcpLimit = int.Parse(tableRow[2]);\n\n                                return new KeyValuePair<int, (int, int)>(prefix, (udpLimit, tcpLimit));\n                            },\n                            3, out KeyValuePair<int, (int, int)>[] qpmPrefixLimitsIPv4, '|'))\n                        {\n                            string strQpmPrefixLimitsIPv4 = \"\";\n\n                            if (qpmPrefixLimitsIPv4.Length == 0)\n                            {\n                                _dnsWebService._dnsServer.QpmPrefixLimitsIPv4 = null;\n                            }\n                            else\n                            {\n                                Dictionary<int, (int, int)> qpmPrefixLimitsIPv4Map = new Dictionary<int, (int, int)>(qpmPrefixLimitsIPv4.Length);\n\n                                foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in qpmPrefixLimitsIPv4)\n                                {\n                                    qpmPrefixLimitsIPv4Map.Add(qpmPrefixLimit.Key, qpmPrefixLimit.Value);\n\n                                    if (strQpmPrefixLimitsIPv4.Length == 0)\n                                        strQpmPrefixLimitsIPv4 = qpmPrefixLimit.Key + \"|\" + qpmPrefixLimit.Value.Item1 + \"|\" + qpmPrefixLimit.Value.Item2;\n                                    else\n                                        strQpmPrefixLimitsIPv4 += \"|\" + qpmPrefixLimit.Key + \"|\" + qpmPrefixLimit.Value.Item1 + \"|\" + qpmPrefixLimit.Value.Item2;\n                                }\n\n                                _dnsWebService._dnsServer.QpmPrefixLimitsIPv4 = qpmPrefixLimitsIPv4Map;\n                            }\n\n                            clusterParameters.Add(\"qpmPrefixLimitsIPv4\", strQpmPrefixLimitsIPv4);\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"qpmPrefixLimitsIPv6\", delegate (JsonElement jsonObject)\n                        {\n                            int prefix = jsonObject.GetProperty(\"prefix\").GetInt32();\n                            int udpLimit = jsonObject.GetProperty(\"udpLimit\").GetInt32();\n                            int tcpLimit = jsonObject.GetProperty(\"tcpLimit\").GetInt32();\n\n                            return new KeyValuePair<int, (int, int)>(prefix, (udpLimit, tcpLimit));\n                        }, delegate (ArraySegment<string> tableRow)\n                        {\n                            int prefix = int.Parse(tableRow[0]);\n                            int udpLimit = int.Parse(tableRow[1]);\n                            int tcpLimit = int.Parse(tableRow[2]);\n\n                            return new KeyValuePair<int, (int, int)>(prefix, (udpLimit, tcpLimit));\n                        },\n                            3, out KeyValuePair<int, (int, int)>[] qpmPrefixLimitsIPv6, '|'))\n                        {\n                            string strQpmPrefixLimitsIPv6 = \"\";\n\n                            if (qpmPrefixLimitsIPv6.Length == 0)\n                            {\n                                _dnsWebService._dnsServer.QpmPrefixLimitsIPv6 = null;\n                            }\n                            else\n                            {\n                                Dictionary<int, (int, int)> qpmPrefixLimitsIPv6Map = new Dictionary<int, (int, int)>(qpmPrefixLimitsIPv6.Length);\n\n                                foreach (KeyValuePair<int, (int, int)> qpmPrefixLimit in qpmPrefixLimitsIPv6)\n                                {\n                                    qpmPrefixLimitsIPv6Map.Add(qpmPrefixLimit.Key, qpmPrefixLimit.Value);\n\n                                    if (strQpmPrefixLimitsIPv6.Length == 0)\n                                        strQpmPrefixLimitsIPv6 = qpmPrefixLimit.Key + \"|\" + qpmPrefixLimit.Value.Item1 + \"|\" + qpmPrefixLimit.Value.Item2;\n                                    else\n                                        strQpmPrefixLimitsIPv6 += \"|\" + qpmPrefixLimit.Key + \"|\" + qpmPrefixLimit.Value.Item1 + \"|\" + qpmPrefixLimit.Value.Item2;\n                                }\n\n                                _dnsWebService._dnsServer.QpmPrefixLimitsIPv6 = qpmPrefixLimitsIPv6Map;\n                            }\n\n                            clusterParameters.Add(\"qpmPrefixLimitsIPv6\", strQpmPrefixLimitsIPv6);\n                        }\n\n                        if (request.TryGetQueryOrForm(\"qpmLimitSampleMinutes\", int.Parse, out int qpmLimitSampleMinutes))\n                        {\n                            _dnsWebService._dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes;\n\n                            clusterParameters.Add(\"qpmLimitSampleMinutes\", qpmLimitSampleMinutes.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"qpmLimitUdpTruncationPercentage\", int.Parse, out int qpmLimitUdpTruncationPercentage))\n                        {\n                            _dnsWebService._dnsServer.QpmLimitUdpTruncationPercentage = qpmLimitUdpTruncationPercentage;\n\n                            clusterParameters.Add(\"qpmLimitUdpTruncationPercentage\", qpmLimitUdpTruncationPercentage.ToString());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"qpmLimitBypassList\", NetworkAddress.Parse, out NetworkAddress[] qpmLimitBypassList))\n                        {\n                            _dnsWebService._dnsServer.QpmLimitBypassList = qpmLimitBypassList;\n\n                            clusterParameters.Add(\"qpmLimitBypassList\", qpmLimitBypassList.Join());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"clientTimeout\", int.Parse, out int clientTimeout))\n                        {\n                            _dnsWebService._dnsServer.ClientTimeout = clientTimeout;\n\n                            clusterParameters.Add(\"clientTimeout\", clientTimeout.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"tcpSendTimeout\", int.Parse, out int tcpSendTimeout))\n                        {\n                            if (_dnsWebService._dnsServer.TcpSendTimeout != tcpSendTimeout)\n                            {\n                                _dnsWebService._dnsServer.TcpSendTimeout = tcpSendTimeout;\n                                restartDnsService = true;\n                            }\n\n                            clusterParameters.Add(\"tcpSendTimeout\", tcpSendTimeout.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"tcpReceiveTimeout\", int.Parse, out int tcpReceiveTimeout))\n                        {\n                            if (_dnsWebService._dnsServer.TcpReceiveTimeout != tcpReceiveTimeout)\n                            {\n                                _dnsWebService._dnsServer.TcpReceiveTimeout = tcpReceiveTimeout;\n                                restartDnsService = true;\n                            }\n\n                            clusterParameters.Add(\"tcpReceiveTimeout\", tcpReceiveTimeout.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"quicIdleTimeout\", int.Parse, out int quicIdleTimeout))\n                        {\n                            _dnsWebService._dnsServer.QuicIdleTimeout = quicIdleTimeout;\n\n                            clusterParameters.Add(\"quicIdleTimeout\", quicIdleTimeout.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"quicMaxInboundStreams\", int.Parse, out int quicMaxInboundStreams))\n                        {\n                            _dnsWebService._dnsServer.QuicMaxInboundStreams = quicMaxInboundStreams;\n\n                            clusterParameters.Add(\"quicMaxInboundStreams\", quicMaxInboundStreams.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"listenBacklog\", int.Parse, out int listenBacklog))\n                        {\n                            if (_dnsWebService._dnsServer.ListenBacklog != listenBacklog)\n                            {\n                                _dnsWebService._dnsServer.ListenBacklog = listenBacklog;\n                                restartDnsService = true;\n                            }\n\n                            clusterParameters.Add(\"listenBacklog\", listenBacklog.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"maxConcurrentResolutionsPerCore\", ushort.Parse, out ushort maxConcurrentResolutionsPerCore))\n                        {\n                            _dnsWebService._dnsServer.MaxConcurrentResolutionsPerCore = maxConcurrentResolutionsPerCore;\n\n                            clusterParameters.Add(\"maxConcurrentResolutionsPerCore\", maxConcurrentResolutionsPerCore.ToString());\n                        }\n\n                        #endregion\n\n                        #region web service\n\n                        if (request.TryGetQueryOrFormArray(\"webServiceLocalAddresses\", IPAddress.Parse, out IPAddress[] webServiceLocalAddresses))\n                        {\n                            if (webServiceLocalAddresses.Length == 0)\n                                webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any];\n\n                            if (!_dnsWebService._webServiceLocalAddresses.HasSameItems(webServiceLocalAddresses))\n                            {\n                                webServiceLocalAddressesChanged = true;\n                                restartWebService = true;\n                            }\n\n                            _dnsWebService._webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(webServiceLocalAddresses);\n                        }\n\n                        if (request.TryGetQueryOrForm(\"webServiceHttpPort\", int.Parse, out int webServiceHttpPort))\n                        {\n                            if (_dnsWebService._webServiceHttpPort != webServiceHttpPort)\n                            {\n                                _dnsWebService._webServiceHttpPort = webServiceHttpPort;\n                                restartWebService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"webServiceEnableTls\", bool.Parse, out bool webServiceEnableTls))\n                        {\n                            if (_dnsWebService._webServiceEnableTls != webServiceEnableTls)\n                            {\n                                _dnsWebService._webServiceEnableTls = webServiceEnableTls;\n                                _webServiceEnablingTls = webServiceEnableTls;\n                                restartWebService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"webServiceEnableHttp3\", bool.Parse, out bool webServiceEnableHttp3))\n                        {\n                            if (_dnsWebService._webServiceEnableHttp3 != webServiceEnableHttp3)\n                            {\n                                if (webServiceEnableHttp3)\n                                    DnsWebService.ValidateQuicSupport(\"HTTP/3\");\n\n                                _dnsWebService._webServiceEnableHttp3 = webServiceEnableHttp3;\n                                restartWebService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"webServiceHttpToTlsRedirect\", bool.Parse, out bool webServiceHttpToTlsRedirect))\n                        {\n                            if (_dnsWebService._webServiceHttpToTlsRedirect != webServiceHttpToTlsRedirect)\n                            {\n                                _dnsWebService._webServiceHttpToTlsRedirect = webServiceHttpToTlsRedirect;\n                                restartWebService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"webServiceUseSelfSignedTlsCertificate\", bool.Parse, out bool webServiceUseSelfSignedTlsCertificate))\n                            _dnsWebService._webServiceUseSelfSignedTlsCertificate = webServiceUseSelfSignedTlsCertificate;\n\n                        if (request.TryGetQueryOrForm(\"webServiceTlsPort\", int.Parse, out int webServiceTlsPort))\n                        {\n                            if (_dnsWebService._webServiceTlsPort != webServiceTlsPort)\n                            {\n                                _dnsWebService._webServiceTlsPort = webServiceTlsPort;\n                                restartWebService = true;\n                            }\n                        }\n\n                        string webServiceTlsCertificatePath = request.QueryOrForm(\"webServiceTlsCertificatePath\");\n                        if (webServiceTlsCertificatePath is not null)\n                        {\n                            if (webServiceTlsCertificatePath.Length == 0)\n                            {\n                                if (!string.IsNullOrEmpty(_dnsWebService._webServiceTlsCertificatePath))\n                                {\n                                    _dnsWebService.RemoveWebServiceTlsCertificate();\n                                    webServiceTlsCertificateChanged = true;\n                                }\n                            }\n                            else\n                            {\n                                string webServiceTlsCertificatePassword = request.QueryOrForm(\"webServiceTlsCertificatePassword\");\n\n                                if ((webServiceTlsCertificatePassword is null) || (webServiceTlsCertificatePassword == \"************\"))\n                                    webServiceTlsCertificatePassword = _dnsWebService._webServiceTlsCertificatePassword;\n\n                                if ((webServiceTlsCertificatePath != _dnsWebService._webServiceTlsCertificatePath) || (webServiceTlsCertificatePassword != _dnsWebService._webServiceTlsCertificatePassword))\n                                {\n                                    _dnsWebService.SetWebServiceTlsCertificate(webServiceTlsCertificatePath, webServiceTlsCertificatePassword);\n                                    webServiceTlsCertificateChanged = true;\n                                }\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"webServiceRealIpHeader\", out string webServiceRealIpHeader))\n                        {\n                            if (webServiceRealIpHeader.Length > 255)\n                                throw new ArgumentException(\"Web service Real IP header name cannot exceed 255 characters.\", nameof(webServiceRealIpHeader));\n\n                            if (webServiceRealIpHeader.Contains(' '))\n                                throw new ArgumentException(\"Web service Real IP header name cannot contain invalid characters.\", nameof(webServiceRealIpHeader));\n\n                            _dnsWebService._webServiceRealIpHeader = webServiceRealIpHeader;\n                        }\n\n                        #endregion\n\n                        #region optional protocols\n\n                        if (request.TryGetQueryOrForm(\"enableDnsOverUdpProxy\", bool.Parse, out bool enableDnsOverUdpProxy))\n                        {\n                            if (_dnsWebService._dnsServer.EnableDnsOverUdpProxy != enableDnsOverUdpProxy)\n                            {\n                                _dnsWebService._dnsServer.EnableDnsOverUdpProxy = enableDnsOverUdpProxy;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"enableDnsOverTcpProxy\", bool.Parse, out bool enableDnsOverTcpProxy))\n                        {\n                            if (_dnsWebService._dnsServer.EnableDnsOverTcpProxy != enableDnsOverTcpProxy)\n                            {\n                                _dnsWebService._dnsServer.EnableDnsOverTcpProxy = enableDnsOverTcpProxy;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"enableDnsOverHttp\", bool.Parse, out bool enableDnsOverHttp))\n                        {\n                            if (_dnsWebService._dnsServer.EnableDnsOverHttp != enableDnsOverHttp)\n                            {\n                                _dnsWebService._dnsServer.EnableDnsOverHttp = enableDnsOverHttp;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"enableDnsOverTls\", bool.Parse, out bool enableDnsOverTls))\n                        {\n                            if (_dnsWebService._dnsServer.EnableDnsOverTls != enableDnsOverTls)\n                            {\n                                _dnsWebService._dnsServer.EnableDnsOverTls = enableDnsOverTls;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"enableDnsOverHttps\", bool.Parse, out bool enableDnsOverHttps))\n                        {\n                            if (_dnsWebService._dnsServer.EnableDnsOverHttps != enableDnsOverHttps)\n                            {\n                                _dnsWebService._dnsServer.EnableDnsOverHttps = enableDnsOverHttps;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"enableDnsOverHttp3\", bool.Parse, out bool enableDnsOverHttp3))\n                        {\n                            if (_dnsWebService._dnsServer.EnableDnsOverHttp3 != enableDnsOverHttp3)\n                            {\n                                if (enableDnsOverHttp3)\n                                    DnsWebService.ValidateQuicSupport(\"DNS-over-HTTP/3\");\n\n                                _dnsWebService._dnsServer.EnableDnsOverHttp3 = enableDnsOverHttp3;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"enableDnsOverQuic\", bool.Parse, out bool enableDnsOverQuic))\n                        {\n                            if (_dnsWebService._dnsServer.EnableDnsOverQuic != enableDnsOverQuic)\n                            {\n                                if (enableDnsOverQuic)\n                                    DnsWebService.ValidateQuicSupport();\n\n                                _dnsWebService._dnsServer.EnableDnsOverQuic = enableDnsOverQuic;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsOverUdpProxyPort\", int.Parse, out int dnsOverUdpProxyPort))\n                        {\n                            if (_dnsWebService._dnsServer.DnsOverUdpProxyPort != dnsOverUdpProxyPort)\n                            {\n                                _dnsWebService._dnsServer.DnsOverUdpProxyPort = dnsOverUdpProxyPort;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsOverTcpProxyPort\", int.Parse, out int dnsOverTcpProxyPort))\n                        {\n                            if (_dnsWebService._dnsServer.DnsOverTcpProxyPort != dnsOverTcpProxyPort)\n                            {\n                                _dnsWebService._dnsServer.DnsOverTcpProxyPort = dnsOverTcpProxyPort;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsOverHttpPort\", int.Parse, out int dnsOverHttpPort))\n                        {\n                            if (_dnsWebService._dnsServer.DnsOverHttpPort != dnsOverHttpPort)\n                            {\n                                _dnsWebService._dnsServer.DnsOverHttpPort = dnsOverHttpPort;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsOverTlsPort\", int.Parse, out int dnsOverTlsPort))\n                        {\n                            if (_dnsWebService._dnsServer.DnsOverTlsPort != dnsOverTlsPort)\n                            {\n                                _dnsWebService._dnsServer.DnsOverTlsPort = dnsOverTlsPort;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsOverHttpsPort\", int.Parse, out int dnsOverHttpsPort))\n                        {\n                            if (_dnsWebService._dnsServer.DnsOverHttpsPort != dnsOverHttpsPort)\n                            {\n                                _dnsWebService._dnsServer.DnsOverHttpsPort = dnsOverHttpsPort;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsOverQuicPort\", int.Parse, out int dnsOverQuicPort))\n                        {\n                            if (_dnsWebService._dnsServer.DnsOverQuicPort != dnsOverQuicPort)\n                            {\n                                _dnsWebService._dnsServer.DnsOverQuicPort = dnsOverQuicPort;\n                                restartDnsService = true;\n                            }\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"reverseProxyNetworkACL\", NetworkAccessControl.Parse, out NetworkAccessControl[] reverseProxyNetworkACL))\n                            _dnsWebService._dnsServer.ReverseProxyNetworkACL = reverseProxyNetworkACL;\n\n                        string dnsTlsCertificatePath = request.QueryOrForm(\"dnsTlsCertificatePath\");\n                        if (dnsTlsCertificatePath is not null)\n                        {\n                            if (dnsTlsCertificatePath.Length == 0)\n                            {\n                                if (!string.IsNullOrEmpty(_dnsWebService._dnsServer.DnsTlsCertificatePath) && (_dnsWebService._dnsServer.EnableDnsOverTls || _dnsWebService._dnsServer.EnableDnsOverHttps || _dnsWebService._dnsServer.EnableDnsOverQuic))\n                                    restartDnsService = true;\n\n                                _dnsWebService._dnsServer.RemoveDnsTlsCertificate();\n                            }\n                            else\n                            {\n                                string dnsTlsCertificatePassword = request.QueryOrForm(\"dnsTlsCertificatePassword\");\n\n                                if ((dnsTlsCertificatePassword is null) || (dnsTlsCertificatePassword == \"************\"))\n                                    dnsTlsCertificatePassword = _dnsWebService._dnsServer.DnsTlsCertificatePassword;\n\n                                if ((dnsTlsCertificatePath != _dnsWebService._dnsServer.DnsTlsCertificatePath) || (dnsTlsCertificatePassword != _dnsWebService._dnsServer.DnsTlsCertificatePassword))\n                                {\n                                    _dnsWebService._dnsServer.SetDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword);\n\n                                    if (string.IsNullOrEmpty(_dnsWebService._dnsServer.DnsTlsCertificatePath) && (_dnsWebService._dnsServer.EnableDnsOverTls || _dnsWebService._dnsServer.EnableDnsOverHttps || _dnsWebService._dnsServer.EnableDnsOverQuic))\n                                        restartDnsService = true;\n                                }\n                            }\n                        }\n\n                        if (request.TryGetQueryOrForm(\"dnsOverHttpRealIpHeader\", out string dnsOverHttpRealIpHeader))\n                            _dnsWebService._dnsServer.DnsOverHttpRealIpHeader = dnsOverHttpRealIpHeader;\n\n                        #endregion\n\n                        #region tsig\n\n                        if (request.TryGetQueryOrFormArray(\"tsigKeys\", delegate (JsonElement jsonObject)\n                            {\n                                string keyName = jsonObject.GetProperty(\"keyName\").GetString().TrimEnd('.').ToLowerInvariant();\n                                string sharedSecret = jsonObject.GetProperty(\"sharedSecret\").GetString();\n                                string algorithmName = jsonObject.GetProperty(\"algorithmName\").GetString();\n\n                                if (DnsClient.IsDomainNameUnicode(keyName))\n                                    keyName = DnsClient.ConvertDomainNameToAscii(keyName);\n\n                                DnsClient.IsDomainNameValid(keyName, true);\n\n                                if (sharedSecret.Length == 0)\n                                    return new TsigKey(keyName, algorithmName);\n\n                                return new TsigKey(keyName, sharedSecret, algorithmName);\n                            },\n                            delegate (ArraySegment<string> tableRow)\n                            {\n                                string keyName = tableRow[0].TrimEnd('.').ToLowerInvariant();\n                                string sharedSecret = tableRow[1];\n                                string algorithmName = tableRow[2];\n\n                                if (DnsClient.IsDomainNameUnicode(keyName))\n                                    keyName = DnsClient.ConvertDomainNameToAscii(keyName);\n\n                                DnsClient.IsDomainNameValid(keyName, true);\n\n                                if (sharedSecret.Length == 0)\n                                    return new TsigKey(keyName, algorithmName);\n\n                                return new TsigKey(keyName, sharedSecret, algorithmName);\n                            },\n                            3, out TsigKey[] tsigKeys, '|')\n                        )\n                        {\n                            string strTsigKeys = \"\";\n\n                            if (tsigKeys.Length == 0)\n                            {\n                                if (_dnsWebService._clusterManager.ClusterInitialized)\n                                    throw new DnsWebServiceException($\"Cannot remove TSIG key for 'cluster-catalog.{_dnsWebService._clusterManager.ClusterDomain}' Cluster Catalog zone.\");\n\n                                _dnsWebService._dnsServer.TsigKeys = null;\n                            }\n                            else\n                            {\n                                Dictionary<string, TsigKey> tsigKeysMap = new Dictionary<string, TsigKey>(tsigKeys.Length);\n\n                                foreach (TsigKey tsigKey in tsigKeys)\n                                {\n                                    tsigKeysMap.Add(tsigKey.KeyName, tsigKey);\n\n                                    if (strTsigKeys.Length == 0)\n                                        strTsigKeys = tsigKey.KeyName + \"|\" + tsigKey.SharedSecret + \"|\" + tsigKey.AlgorithmName;\n                                    else\n                                        strTsigKeys += \"|\" + tsigKey.KeyName + \"|\" + tsigKey.SharedSecret + \"|\" + tsigKey.AlgorithmName;\n                                }\n\n                                if (_dnsWebService._clusterManager.ClusterInitialized)\n                                {\n                                    if (!tsigKeysMap.ContainsKey($\"cluster-catalog.{_dnsWebService._clusterManager.ClusterDomain}\"))\n                                        throw new DnsWebServiceException($\"Cannot remove TSIG key for 'cluster-catalog.{_dnsWebService._clusterManager.ClusterDomain}' Cluster Catalog zone.\");\n                                }\n\n                                _dnsWebService._dnsServer.TsigKeys = tsigKeysMap;\n                            }\n\n                            clusterParameters.Add(\"tsigKeys\", strTsigKeys);\n                        }\n\n                        #endregion\n\n                        #region recursion\n\n                        if (request.TryGetQueryOrFormEnum(\"recursion\", out DnsServerRecursion recursion))\n                        {\n                            _dnsWebService._dnsServer.Recursion = recursion;\n\n                            clusterParameters.Add(\"recursion\", recursion.ToString());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"recursionNetworkACL\", NetworkAccessControl.Parse, out NetworkAccessControl[] recursionNetworkACL))\n                        {\n                            _dnsWebService._dnsServer.RecursionNetworkACL = recursionNetworkACL;\n\n                            clusterParameters.Add(\"recursionNetworkACL\", recursionNetworkACL.Join());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"randomizeName\", bool.Parse, out bool randomizeName))\n                        {\n                            _dnsWebService._dnsServer.RandomizeName = randomizeName;\n\n                            clusterParameters.Add(\"randomizeName\", randomizeName.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"qnameMinimization\", bool.Parse, out bool qnameMinimization))\n                        {\n                            _dnsWebService._dnsServer.QnameMinimization = qnameMinimization;\n\n                            clusterParameters.Add(\"qnameMinimization\", qnameMinimization.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"resolverRetries\", int.Parse, out int resolverRetries))\n                        {\n                            _dnsWebService._dnsServer.ResolverRetries = resolverRetries;\n\n                            clusterParameters.Add(\"resolverRetries\", resolverRetries.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"resolverTimeout\", int.Parse, out int resolverTimeout))\n                        {\n                            _dnsWebService._dnsServer.ResolverTimeout = resolverTimeout;\n\n                            clusterParameters.Add(\"resolverTimeout\", resolverTimeout.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"resolverConcurrency\", int.Parse, out int resolverConcurrency))\n                        {\n                            _dnsWebService._dnsServer.ResolverConcurrency = resolverConcurrency;\n\n                            clusterParameters.Add(\"resolverConcurrency\", resolverConcurrency.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"resolverMaxStackCount\", int.Parse, out int resolverMaxStackCount))\n                        {\n                            _dnsWebService._dnsServer.ResolverMaxStackCount = resolverMaxStackCount;\n\n                            clusterParameters.Add(\"resolverMaxStackCount\", resolverMaxStackCount.ToString());\n                        }\n\n                        #endregion\n\n                        #region cache\n\n                        //cache\n                        if (request.TryGetQueryOrForm(\"saveCache\", bool.Parse, out bool saveCache))\n                            _dnsWebService._dnsServer.SaveCacheToDisk = saveCache;\n\n                        if (request.TryGetQueryOrForm(\"serveStale\", bool.Parse, out bool serveStale))\n                            _dnsWebService._dnsServer.ServeStale = serveStale;\n\n                        if (request.TryGetQueryOrForm(\"serveStaleTtl\", ZoneFile.ParseTtl, out uint serveStaleTtl))\n                            _dnsWebService._dnsServer.CacheZoneManager.ServeStaleTtl = serveStaleTtl;\n\n                        if (request.TryGetQueryOrForm(\"serveStaleAnswerTtl\", ZoneFile.ParseTtl, out uint serveStaleAnswerTtl))\n                            _dnsWebService._dnsServer.CacheZoneManager.ServeStaleAnswerTtl = serveStaleAnswerTtl;\n\n                        if (request.TryGetQueryOrForm(\"serveStaleResetTtl\", ZoneFile.ParseTtl, out uint serveStaleResetTtl))\n                            _dnsWebService._dnsServer.CacheZoneManager.ServeStaleResetTtl = serveStaleResetTtl;\n\n                        if (request.TryGetQueryOrForm(\"serveStaleMaxWaitTime\", int.Parse, out int serveStaleMaxWaitTime))\n                            _dnsWebService._dnsServer.ServeStaleMaxWaitTime = serveStaleMaxWaitTime;\n\n                        if (request.TryGetQueryOrForm(\"cacheMaximumEntries\", long.Parse, out long cacheMaximumEntries))\n                            _dnsWebService._dnsServer.CacheZoneManager.MaximumEntries = cacheMaximumEntries;\n\n                        if (request.TryGetQueryOrForm(\"cacheMinimumRecordTtl\", ZoneFile.ParseTtl, out uint cacheMinimumRecordTtl))\n                            _dnsWebService._dnsServer.CacheZoneManager.MinimumRecordTtl = cacheMinimumRecordTtl;\n\n                        if (request.TryGetQueryOrForm(\"cacheMaximumRecordTtl\", ZoneFile.ParseTtl, out uint cacheMaximumRecordTtl))\n                            _dnsWebService._dnsServer.CacheZoneManager.MaximumRecordTtl = cacheMaximumRecordTtl;\n\n                        if (request.TryGetQueryOrForm(\"cacheNegativeRecordTtl\", ZoneFile.ParseTtl, out uint cacheNegativeRecordTtl))\n                            _dnsWebService._dnsServer.CacheZoneManager.NegativeRecordTtl = cacheNegativeRecordTtl;\n\n                        if (request.TryGetQueryOrForm(\"cacheFailureRecordTtl\", ZoneFile.ParseTtl, out uint cacheFailureRecordTtl))\n                            _dnsWebService._dnsServer.CacheZoneManager.FailureRecordTtl = cacheFailureRecordTtl;\n\n                        if (request.TryGetQueryOrForm(\"cachePrefetchEligibility\", int.Parse, out int cachePrefetchEligibility))\n                            _dnsWebService._dnsServer.CachePrefetchEligibility = cachePrefetchEligibility;\n\n                        if (request.TryGetQueryOrForm(\"cachePrefetchTrigger\", int.Parse, out int cachePrefetchTrigger))\n                            _dnsWebService._dnsServer.CachePrefetchTrigger = cachePrefetchTrigger;\n\n                        if (request.TryGetQueryOrForm(\"cachePrefetchSampleIntervalInMinutes\", int.Parse, out int cachePrefetchSampleIntervalMinutes))\n                            _dnsWebService._dnsServer.CachePrefetchSampleIntervalMinutes = cachePrefetchSampleIntervalMinutes;\n\n                        if (request.TryGetQueryOrForm(\"cachePrefetchSampleEligibilityHitsPerHour\", int.Parse, out int cachePrefetchSampleEligibilityHitsPerHour))\n                            _dnsWebService._dnsServer.CachePrefetchSampleEligibilityHitsPerHour = cachePrefetchSampleEligibilityHitsPerHour;\n\n                        #endregion\n\n                        #region blocking\n\n                        if (request.TryGetQueryOrForm(\"enableBlocking\", bool.Parse, out bool enableBlocking))\n                        {\n                            _dnsWebService._dnsServer.EnableBlocking = enableBlocking;\n\n                            clusterParameters.Add(\"enableBlocking\", enableBlocking.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"allowTxtBlockingReport\", bool.Parse, out bool allowTxtBlockingReport))\n                        {\n                            _dnsWebService._dnsServer.AllowTxtBlockingReport = allowTxtBlockingReport;\n\n                            clusterParameters.Add(\"allowTxtBlockingReport\", allowTxtBlockingReport.ToString());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"blockingBypassList\", NetworkAddress.Parse, out NetworkAddress[] blockingBypassList))\n                        {\n                            _dnsWebService._dnsServer.BlockingBypassList = blockingBypassList;\n\n                            clusterParameters.Add(\"blockingBypassList\", blockingBypassList.Join());\n                        }\n\n                        if (request.TryGetQueryOrFormEnum(\"blockingType\", out DnsServerBlockingType blockingType))\n                        {\n                            _dnsWebService._dnsServer.BlockingType = blockingType;\n\n                            clusterParameters.Add(\"blockingType\", blockingType.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"blockingAnswerTtl\", ZoneFile.ParseTtl, out uint blockingAnswerTtl))\n                        {\n                            _dnsWebService._dnsServer.BlockingAnswerTtl = blockingAnswerTtl;\n\n                            clusterParameters.Add(\"blockingAnswerTtl\", blockingAnswerTtl.ToString());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"customBlockingAddresses\", out string[] customBlockingAddresses))\n                        {\n                            if (customBlockingAddresses.Length == 0)\n                            {\n                                _dnsWebService._dnsServer.CustomBlockingARecords = null;\n                                _dnsWebService._dnsServer.CustomBlockingAAAARecords = null;\n                            }\n                            else\n                            {\n                                List<DnsARecordData> dnsARecords = new List<DnsARecordData>();\n                                List<DnsAAAARecordData> dnsAAAARecords = new List<DnsAAAARecordData>();\n\n                                foreach (string strAddress in customBlockingAddresses)\n                                {\n                                    if (IPAddress.TryParse(strAddress, out IPAddress customAddress))\n                                    {\n                                        switch (customAddress.AddressFamily)\n                                        {\n                                            case AddressFamily.InterNetwork:\n                                                dnsARecords.Add(new DnsARecordData(customAddress));\n                                                break;\n\n                                            case AddressFamily.InterNetworkV6:\n                                                dnsAAAARecords.Add(new DnsAAAARecordData(customAddress));\n                                                break;\n                                        }\n                                    }\n                                }\n\n                                _dnsWebService._dnsServer.CustomBlockingARecords = dnsARecords;\n                                _dnsWebService._dnsServer.CustomBlockingAAAARecords = dnsAAAARecords;\n                            }\n\n                            clusterParameters.Add(\"customBlockingAddresses\", customBlockingAddresses.Join());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"blockListUrls\", out string[] blockListUrls))\n                        {\n                            _dnsWebService._dnsServer.BlockListZoneManager.BlockListUrls = blockListUrls;\n                            _dnsWebService._dnsServer.BlockListZoneManager.SaveConfigFile();\n\n                            clusterParameters.Add(\"blockListUrls\", blockListUrls.Join());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"blockListUpdateIntervalHours\", int.Parse, out int blockListUpdateIntervalHours))\n                        {\n                            _dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = blockListUpdateIntervalHours;\n                            _dnsWebService._dnsServer.BlockListZoneManager.SaveConfigFile();\n\n                            clusterParameters.Add(\"blockListUpdateIntervalHours\", blockListUpdateIntervalHours.ToString());\n                        }\n\n                        #endregion\n\n                        #region proxy & forwarders\n\n                        //proxy & forwarders\n                        if (request.TryGetQueryOrFormEnum(\"proxyType\", out NetProxyType proxyType))\n                        {\n                            if (proxyType == NetProxyType.None)\n                            {\n                                _dnsWebService._dnsServer.Proxy = null;\n                            }\n                            else\n                            {\n                                NetworkCredential credential = null;\n\n                                if (request.TryGetQueryOrForm(\"proxyUsername\", out string proxyUsername))\n                                {\n                                    if (proxyUsername.Length > 255)\n                                        throw new ArgumentException(\"Proxy username length cannot exceed 255 characters.\", nameof(proxyUsername));\n\n                                    string proxyPassword = request.QueryOrForm(\"proxyPassword\");\n                                    if (proxyPassword?.Length > 255)\n                                        throw new ArgumentException(\"Proxy password length cannot exceed 255 characters.\", nameof(proxyPassword));\n\n                                    credential = new NetworkCredential(proxyUsername, proxyPassword);\n\n                                    clusterParameters.Add(\"proxyUsername\", proxyUsername);\n                                    clusterParameters.Add(\"proxyPassword\", proxyPassword ?? \"\");\n                                }\n\n                                string proxyAddress = request.QueryOrForm(\"proxyAddress\");\n                                string proxyPort = request.QueryOrForm(\"proxyPort\");\n\n                                _dnsWebService._dnsServer.Proxy = NetProxy.CreateProxy(proxyType, proxyAddress, int.Parse(proxyPort), credential);\n\n                                clusterParameters.Add(\"proxyAddress\", proxyAddress);\n                                clusterParameters.Add(\"proxyPort\", proxyPort);\n\n                                if (request.TryGetQueryOrFormArray(\"proxyBypass\", delegate (string value) { return new NetProxyBypassItem(value); }, out NetProxyBypassItem[] proxyBypass))\n                                {\n                                    _dnsWebService._dnsServer.Proxy.BypassList = proxyBypass;\n\n                                    clusterParameters.Add(\"proxyBypass\", proxyBypass.Join());\n                                }\n                            }\n\n                            clusterParameters.Add(\"proxyType\", proxyType.ToString());\n                        }\n\n                        if (request.TryGetQueryOrFormArray(\"forwarders\", NameServerAddress.Parse, out NameServerAddress[] forwarders))\n                        {\n                            if (forwarders.Length == 0)\n                            {\n                                _dnsWebService._dnsServer.Forwarders = null;\n                            }\n                            else\n                            {\n                                DnsTransportProtocol forwarderProtocol = request.GetQueryOrFormEnum(\"forwarderProtocol\", DnsTransportProtocol.Udp);\n\n                                switch (forwarderProtocol)\n                                {\n                                    case DnsTransportProtocol.Udp:\n                                        if (proxyType == NetProxyType.Http)\n                                            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.\");\n\n                                        break;\n\n                                    case DnsTransportProtocol.HttpsJson:\n                                        forwarderProtocol = DnsTransportProtocol.Https;\n                                        break;\n\n                                    case DnsTransportProtocol.Quic:\n                                        DnsWebService.ValidateQuicSupport();\n\n                                        if (proxyType == NetProxyType.Http)\n                                            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.\");\n\n                                        break;\n                                }\n\n                                for (int i = 0; i < forwarders.Length; i++)\n                                {\n                                    if (forwarders[i].Protocol != forwarderProtocol)\n                                        forwarders[i] = forwarders[i].Clone(forwarderProtocol);\n                                }\n\n                                if (!_dnsWebService._dnsServer.Forwarders.ListEquals(forwarders))\n                                    _dnsWebService._dnsServer.Forwarders = forwarders;\n\n                                clusterParameters.Add(\"forwarderProtocol\", forwarderProtocol.ToString());\n                            }\n\n                            clusterParameters.Add(\"forwarders\", forwarders.Join());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"concurrentForwarding\", bool.Parse, out bool concurrentForwarding))\n                        {\n                            _dnsWebService._dnsServer.ConcurrentForwarding = concurrentForwarding;\n\n                            clusterParameters.Add(\"concurrentForwarding\", concurrentForwarding.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"forwarderRetries\", int.Parse, out int forwarderRetries))\n                        {\n                            _dnsWebService._dnsServer.ForwarderRetries = forwarderRetries;\n\n                            clusterParameters.Add(\"forwarderRetries\", forwarderRetries.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"forwarderTimeout\", int.Parse, out int forwarderTimeout))\n                        {\n                            _dnsWebService._dnsServer.ForwarderTimeout = forwarderTimeout;\n\n                            clusterParameters.Add(\"forwarderTimeout\", forwarderTimeout.ToString());\n                        }\n\n                        if (request.TryGetQueryOrForm(\"forwarderConcurrency\", int.Parse, out int forwarderConcurrency))\n                        {\n                            _dnsWebService._dnsServer.ForwarderConcurrency = forwarderConcurrency;\n\n                            clusterParameters.Add(\"forwarderConcurrency\", forwarderConcurrency.ToString());\n                        }\n\n                        #endregion\n\n                        #region logging\n\n                        if (request.TryGetQueryOrFormEnum(\"loggingType\", out LoggingType loggingType))\n                            _dnsWebService._log.LoggingType = loggingType;\n                        else if (request.TryGetQueryOrForm(\"enableLogging\", bool.Parse, out bool enableLogging))\n                            _dnsWebService._log.LoggingType = enableLogging ? LoggingType.File : LoggingType.None;\n\n                        if (request.TryGetQueryOrForm(\"ignoreResolverLogs\", bool.Parse, out bool ignoreResolverLogs))\n                            _dnsWebService._dnsServer.ResolverLogManager = ignoreResolverLogs ? null : _dnsWebService._log;\n\n                        if (request.TryGetQueryOrForm(\"logQueries\", bool.Parse, out bool logQueries))\n                            _dnsWebService._dnsServer.QueryLogManager = logQueries ? _dnsWebService._log : null;\n\n                        if (request.TryGetQueryOrForm(\"useLocalTime\", bool.Parse, out bool useLocalTime))\n                            _dnsWebService._log.UseLocalTime = useLocalTime;\n\n                        if (request.TryGetQueryOrForm(\"logFolder\", out string logFolder))\n                            _dnsWebService._log.LogFolder = logFolder;\n\n                        if (request.TryGetQueryOrForm(\"maxLogFileDays\", int.Parse, out int maxLogFileDays))\n                            _dnsWebService._log.MaxLogFileDays = maxLogFileDays;\n\n                        if (request.TryGetQueryOrForm(\"enableInMemoryStats\", bool.Parse, out bool enableInMemoryStats))\n                            _dnsWebService._dnsServer.StatsManager.EnableInMemoryStats = enableInMemoryStats;\n\n                        if (request.TryGetQueryOrForm(\"maxStatFileDays\", int.Parse, out int maxStatFileDays))\n                            _dnsWebService._dnsServer.StatsManager.MaxStatFileDays = maxStatFileDays;\n\n                        #endregion\n                    }\n                    finally\n                    {\n                        jsonDocument?.Dispose();\n\n                        //enforce cluster mandatory TLS requirement\n                        if (_dnsWebService._clusterManager.ClusterInitialized)\n                        {\n                            if (!_dnsWebService._webServiceEnableTls || string.IsNullOrEmpty(_dnsWebService._webServiceTlsCertificatePath))\n                            {\n                                //force enable TLS with self-signed certificate if cluster is initialized\n                                _dnsWebService._webServiceEnableTls = true;\n                                _dnsWebService._webServiceUseSelfSignedTlsCertificate = true;\n                            }\n                        }\n\n                        //TLS actions\n                        _dnsWebService.CheckAndLoadSelfSignedCertificate(serverDomainChanged || webServiceLocalAddressesChanged, true);\n\n                        if (_dnsWebService._webServiceEnableTls && string.IsNullOrEmpty(_dnsWebService._webServiceTlsCertificatePath) && !_dnsWebService._webServiceUseSelfSignedTlsCertificate)\n                        {\n                            //disable TLS\n                            _dnsWebService._webServiceEnableTls = false;\n                            restartWebService = true;\n                        }\n\n                        //cluster update actions\n                        if (_dnsWebService._clusterManager.ClusterInitialized)\n                        {\n                            if (webServiceTlsCertificateChanged || serverDomainChanged || webServiceLocalAddressesChanged)\n                                _dnsWebService._clusterManager.UpdateSelfNodeUrlAndCertificate();\n                        }\n\n                        //save config\n                        _dnsWebService.SaveConfigFile();\n                        _dnsWebService._dnsServer.SaveConfigFile();\n                        _dnsWebService._dnsServer.BlockListZoneManager.SaveConfigFile();\n                        _dnsWebService._log.SaveConfigFile();\n                    }\n\n                    _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNS Settings were updated successfully.\");\n\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                    {\n                        if (_dnsWebService._clusterManager.GetSelfNode().Type == ClusterNodeType.Primary)\n                            _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodes();\n                        else if (clusterParameters.Count > 0)\n                            await _dnsWebService._clusterManager.GetPrimaryNode().SetClusterSettingsAsync(sessionUser, clusterParameters);\n                    }\n\n                    Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                    WriteDnsSettings(jsonWriter);\n                }\n                finally\n                {\n                    if (restartDnsService || restartWebService)\n                        RestartService(restartDnsService, restartWebService, oldWebServiceLocalAddresses, oldWebServiceHttpPort, oldWebServiceTlsPort);\n                }\n            }\n\n            public void GetTsigKeyNames(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (\n                    !_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.View) &&\n                    !_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)\n                   )\n                {\n                    throw new DnsWebServiceException(\"Access was denied.\");\n                }\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"tsigKeyNames\");\n                {\n                    jsonWriter.WriteStartArray();\n\n                    if (_dnsWebService._dnsServer.TsigKeys is not null)\n                    {\n                        foreach (KeyValuePair<string, TsigKey> tsigKey in _dnsWebService._dnsServer.TsigKeys)\n                            jsonWriter.WriteStringValue(tsigKey.Key);\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n            }\n\n            public async Task BackupSettingsAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                bool authConfig = request.GetQueryOrForm(\"authConfig\", bool.Parse, false);\n                bool clusterConfig = request.GetQueryOrForm(\"clusterConfig\", bool.Parse, false);\n                bool webServiceSettings = request.GetQueryOrForm(\"webServiceSettings\", bool.Parse, false);\n                bool dnsSettings = request.GetQueryOrForm(\"dnsSettings\", bool.Parse, false);\n                bool logSettings = request.GetQueryOrForm(\"logSettings\", bool.Parse, false);\n                bool zones = request.GetQueryOrForm(\"zones\", bool.Parse, false);\n                bool allowedZones = request.GetQueryOrForm(\"allowedZones\", bool.Parse, false);\n                bool blockedZones = request.GetQueryOrForm(\"blockedZones\", bool.Parse, false);\n                bool blockLists = request.GetQueryOrForm(\"blockLists\", bool.Parse, false);\n                bool apps = request.GetQueryOrForm(\"apps\", bool.Parse, false);\n                bool scopes = request.GetQueryOrForm(\"scopes\", bool.Parse, false);\n                bool stats = request.GetQueryOrForm(\"stats\", bool.Parse, false);\n                bool logs = request.GetQueryOrForm(\"logs\", bool.Parse, false);\n\n                string tmpFile = Path.GetTempFileName();\n                try\n                {\n                    await using (FileStream backupZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                    {\n                        //create backup zip\n                        await _dnsWebService.BackupConfigAsync(backupZipStream, authConfig, clusterConfig, webServiceSettings, dnsSettings, logSettings, zones, allowedZones, blockedZones, blockLists, apps, scopes, stats, logs);\n\n                        //send zip file\n                        backupZipStream.Position = 0;\n\n                        HttpResponse response = context.Response;\n\n                        response.ContentType = \"application/zip\";\n                        response.ContentLength = backupZipStream.Length;\n                        response.Headers.ContentDisposition = \"attachment;filename=\" + _dnsWebService._dnsServer.ServerDomain + DateTime.UtcNow.ToString(\"_yyyy-MM-dd_HH-mm-ss\") + \"_backup.zip\";\n\n                        await using (Stream output = response.Body)\n                        {\n                            await backupZipStream.CopyToAsync(output);\n                        }\n                    }\n                }\n                finally\n                {\n                    try\n                    {\n                        File.Delete(tmpFile);\n                    }\n                    catch (Exception ex)\n                    {\n                        _dnsWebService._log.Write(ex);\n                    }\n                }\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Settings backup zip file was exported.\");\n            }\n\n            public async Task RestoreSettingsAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                bool authConfig = request.GetQueryOrForm(\"authConfig\", bool.Parse, false);\n                bool clusterConfig = request.GetQueryOrForm(\"clusterConfig\", bool.Parse, false);\n                bool webServiceSettings = request.GetQueryOrForm(\"webServiceSettings\", bool.Parse, false);\n                bool dnsSettings = request.GetQueryOrForm(\"dnsSettings\", bool.Parse, false);\n                bool logSettings = request.GetQueryOrForm(\"logSettings\", bool.Parse, false);\n                bool zones = request.GetQueryOrForm(\"zones\", bool.Parse, false);\n                bool allowedZones = request.GetQueryOrForm(\"allowedZones\", bool.Parse, false);\n                bool blockedZones = request.GetQueryOrForm(\"blockedZones\", bool.Parse, false);\n                bool blockLists = request.GetQueryOrForm(\"blockLists\", bool.Parse, false);\n                bool apps = request.GetQueryOrForm(\"apps\", bool.Parse, false);\n                bool scopes = request.GetQueryOrForm(\"scopes\", bool.Parse, false);\n                bool stats = request.GetQueryOrForm(\"stats\", bool.Parse, false);\n                bool logs = request.GetQueryOrForm(\"logs\", bool.Parse, false);\n                bool deleteExistingFiles = request.GetQueryOrForm(\"deleteExistingFiles\", bool.Parse, false);\n\n                if (!request.HasFormContentType || (request.Form.Files.Count == 0))\n                    throw new DnsWebServiceException(\"DNS backup zip file is missing.\");\n\n                IReadOnlyList<IPAddress> oldWebServiceLocalAddresses = _dnsWebService._webServiceLocalAddresses;\n                int oldWebServiceHttpPort = _dnsWebService._webServiceHttpPort;\n                int oldWebServiceTlsPort = _dnsWebService._webServiceTlsPort;\n\n                try\n                {\n                    //write to temp file\n                    string tmpFile = Path.GetTempFileName();\n                    try\n                    {\n                        await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))\n                        {\n                            await request.Form.Files[0].CopyToAsync(fS);\n\n                            fS.Position = 0;\n\n                            await _dnsWebService.RestoreConfigAsync(fS, authConfig, clusterConfig, webServiceSettings, dnsSettings, logSettings, zones, allowedZones, blockedZones, blockLists, apps, scopes, stats, logs, deleteExistingFiles, context.GetCurrentSession());\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Settings backup zip file was restored.\");\n                        }\n                    }\n                    finally\n                    {\n                        try\n                        {\n                            File.Delete(tmpFile);\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(ex);\n                        }\n                    }\n\n                    //trigger cluster update\n                    if (_dnsWebService._clusterManager.ClusterInitialized)\n                        _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode();\n\n                    Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                    WriteDnsSettings(jsonWriter);\n\n                }\n                finally\n                {\n                    if (dnsSettings || webServiceSettings)\n                        RestartService(dnsSettings, webServiceSettings, oldWebServiceLocalAddresses, oldWebServiceHttpPort, oldWebServiceTlsPort);\n                }\n            }\n\n            public void ForceUpdateBlockLists(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.BlockListZoneManager.ForceUpdateBlockLists();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Block list update was triggered.\");\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    UserSession session = context.GetCurrentSession();\n\n                    if ((session.Type == UserSessionType.ApiToken) && session.TokenName.Equals(_dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))\n                        return; //call from cluster node itself\n\n                    //relay action on all other cluster nodes async\n                    ThreadPool.QueueUserWorkItem(async delegate (object state)\n                    {\n                        try\n                        {\n                            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _dnsWebService._clusterManager.ClusterNodes;\n                            List<Task> tasks = new List<Task>(clusterNodes.Count);\n\n                            foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n                            {\n                                if (clusterNode.Value.State == ClusterNodeState.Self)\n                                    continue;\n\n                                tasks.Add(clusterNode.Value.ForceUpdateBlockListsAsync(sessionUser));\n                            }\n\n                            foreach (Task task in tasks)\n                            {\n                                try\n                                {\n                                    await task;\n                                }\n                                catch (Exception ex)\n                                {\n                                    _dnsWebService._log.Write(ex);\n                                }\n                            }\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(ex);\n                        }\n                    });\n                }\n            }\n\n            public void TemporaryDisableBlocking(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                int minutes = context.Request.GetQueryOrForm(\"minutes\", int.Parse);\n\n                _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlocking(minutes, context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), sessionUser.Username);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                jsonWriter.WriteString(\"temporaryDisableBlockingTill\", _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlockingTill);\n\n                if (_dnsWebService._clusterManager.ClusterInitialized)\n                {\n                    UserSession session = context.GetCurrentSession();\n\n                    if ((session.Type == UserSessionType.ApiToken) && session.TokenName.Equals(_dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))\n                        return; //call from cluster node itself\n\n                    //relay action on all other cluster nodes async\n                    ThreadPool.QueueUserWorkItem(async delegate (object state)\n                    {\n                        try\n                        {\n                            IReadOnlyDictionary<int, ClusterNode> clusterNodes = _dnsWebService._clusterManager.ClusterNodes;\n                            List<Task> tasks = new List<Task>(clusterNodes.Count);\n\n                            foreach (KeyValuePair<int, ClusterNode> clusterNode in clusterNodes)\n                            {\n                                if (clusterNode.Value.State == ClusterNodeState.Self)\n                                    continue;\n\n                                tasks.Add(clusterNode.Value.TemporaryDisableBlockingAsync(sessionUser, minutes));\n                            }\n\n                            foreach (Task task in tasks)\n                            {\n                                try\n                                {\n                                    await task;\n                                }\n                                catch (Exception ex)\n                                {\n                                    _dnsWebService._log.Write(ex);\n                                }\n                            }\n                        }\n                        catch (Exception ex)\n                        {\n                            _dnsWebService._log.Write(ex);\n                        }\n                    });\n                }\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/WebServiceZonesApi.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.Auth;\nusing DnsServerCore.Cluster;\nusing DnsServerCore.Dns;\nusing DnsServerCore.Dns.Dnssec;\nusing DnsServerCore.Dns.ResourceRecords;\nusing DnsServerCore.Dns.ZoneManagers;\nusing DnsServerCore.Dns.Zones;\nusing Microsoft.AspNetCore.Http;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore\n{\n    public partial class DnsWebService\n    {\n        class WebServiceZonesApi\n        {\n            #region variables\n\n            static readonly char[] _commaSeparator = new char[] { ',' };\n            static readonly char[] _pipeSeparator = new char[] { '|' };\n            static readonly char[] _commaSpaceSeparator = new char[] { ',', ' ' };\n            static readonly char[] _newLineSeparator = new char[] { '\\r', '\\n' };\n\n            readonly DnsWebService _dnsWebService;\n\n            #endregion\n\n            #region constructor\n\n            public WebServiceZonesApi(DnsWebService dnsWebService)\n            {\n                _dnsWebService = dnsWebService;\n            }\n\n            #endregion\n\n            #region static\n\n            public static void WriteRecordsAsJson(List<DnsResourceRecord> records, Utf8JsonWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null)\n            {\n                if (records is null)\n                {\n                    jsonWriter.WritePropertyName(\"records\");\n                    jsonWriter.WriteStartArray();\n                    jsonWriter.WriteEndArray();\n\n                    return;\n                }\n\n                records.Sort();\n\n                Dictionary<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> groupedByDomainRecords = DnsResourceRecord.GroupRecords(records);\n\n                jsonWriter.WritePropertyName(\"records\");\n                jsonWriter.WriteStartArray();\n\n                foreach (KeyValuePair<string, Dictionary<DnsResourceRecordType, List<DnsResourceRecord>>> groupedByTypeRecords in groupedByDomainRecords)\n                {\n                    foreach (KeyValuePair<DnsResourceRecordType, List<DnsResourceRecord>> groupedRecords in groupedByTypeRecords.Value)\n                    {\n                        foreach (DnsResourceRecord record in groupedRecords.Value)\n                            WriteRecordAsJson(record, jsonWriter, authoritativeZoneRecords, zoneInfo);\n                    }\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            #endregion\n\n            #region private\n\n            private static void WriteRecordAsJson(DnsResourceRecord record, Utf8JsonWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null)\n            {\n                jsonWriter.WriteStartObject();\n\n                jsonWriter.WriteString(\"name\", record.Name);\n\n                if (DnsClient.TryConvertDomainNameToUnicode(record.Name, out string idn))\n                    jsonWriter.WriteString(\"nameIdn\", idn);\n\n                jsonWriter.WriteString(\"type\", record.Type.ToString());\n\n                if (authoritativeZoneRecords)\n                {\n                    GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo();\n\n                    jsonWriter.WriteNumber(\"ttl\", record.TTL);\n                    jsonWriter.WriteString(\"ttlString\", ZoneFile.GetTtlString(record.TTL));\n                    jsonWriter.WriteBoolean(\"disabled\", authRecordInfo.Disabled);\n\n                    string comments = authRecordInfo.Comments;\n                    if (!string.IsNullOrEmpty(comments))\n                        jsonWriter.WriteString(\"comments\", comments);\n                }\n                else\n                {\n                    if (record.IsStale)\n                        jsonWriter.WriteString(\"ttl\", \"0 (0s)\");\n                    else\n                        jsonWriter.WriteString(\"ttl\", record.TTL + \" (\" + ZoneFile.GetTtlString(record.TTL) + \")\");\n                }\n\n                jsonWriter.WritePropertyName(\"rData\");\n                jsonWriter.WriteStartObject();\n\n                switch (record.Type)\n                {\n                    case DnsResourceRecordType.A:\n                        {\n                            if (record.RDATA is DnsARecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"ipAddress\", rdata.Address.ToString());\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NS:\n                        {\n                            if (record.RDATA is DnsNSRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"nameServer\", rdata.NameServer.Length == 0 ? \".\" : rdata.NameServer);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.NameServer, out string nameServerIdn))\n                                    jsonWriter.WriteString(\"nameServerIdn\", nameServerIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CNAME:\n                        {\n                            if (record.RDATA is DnsCNAMERecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"cname\", rdata.Domain.Length == 0 ? \".\" : rdata.Domain);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string cnameIdn))\n                                    jsonWriter.WriteString(\"cnameIdn\", cnameIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SOA:\n                        {\n                            if (record.RDATA is DnsSOARecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"primaryNameServer\", rdata.PrimaryNameServer);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.PrimaryNameServer, out string primaryNameServerIdn))\n                                    jsonWriter.WriteString(\"primaryNameServerIdn\", primaryNameServerIdn);\n\n                                jsonWriter.WriteString(\"responsiblePerson\", rdata.ResponsiblePerson);\n                                jsonWriter.WriteNumber(\"serial\", rdata.Serial);\n\n                                if (authoritativeZoneRecords)\n                                {\n                                    jsonWriter.WriteNumber(\"refresh\", rdata.Refresh);\n                                    jsonWriter.WriteNumber(\"retry\", rdata.Retry);\n                                    jsonWriter.WriteNumber(\"expire\", rdata.Expire);\n                                    jsonWriter.WriteNumber(\"minimum\", rdata.Minimum);\n\n                                    jsonWriter.WriteString(\"refreshString\", ZoneFile.GetTtlString(rdata.Refresh));\n                                    jsonWriter.WriteString(\"retryString\", ZoneFile.GetTtlString(rdata.Retry));\n                                    jsonWriter.WriteString(\"expireString\", ZoneFile.GetTtlString(rdata.Expire));\n                                    jsonWriter.WriteString(\"minimumString\", ZoneFile.GetTtlString(rdata.Minimum));\n                                }\n                                else\n                                {\n                                    jsonWriter.WriteString(\"refresh\", rdata.Refresh + \" (\" + ZoneFile.GetTtlString(rdata.Refresh) + \")\");\n                                    jsonWriter.WriteString(\"retry\", rdata.Retry + \" (\" + ZoneFile.GetTtlString(rdata.Retry) + \")\");\n                                    jsonWriter.WriteString(\"expire\", rdata.Expire + \" (\" + ZoneFile.GetTtlString(rdata.Expire) + \")\");\n                                    jsonWriter.WriteString(\"minimum\", rdata.Minimum + \" (\" + ZoneFile.GetTtlString(rdata.Minimum) + \")\");\n                                }\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n\n                            if (authoritativeZoneRecords && (zoneInfo is not null))\n                            {\n                                switch (zoneInfo.Type)\n                                {\n                                    case AuthZoneType.Primary:\n                                    case AuthZoneType.Forwarder:\n                                    case AuthZoneType.Catalog:\n                                        jsonWriter.WriteBoolean(\"useSerialDateScheme\", record.GetAuthSOARecordInfo().UseSoaSerialDateScheme);\n                                        break;\n                                }\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.PTR:\n                        {\n                            if (record.RDATA is DnsPTRRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"ptrName\", rdata.Domain.Length == 0 ? \".\" : rdata.Domain);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string ptrNameIdn))\n                                    jsonWriter.WriteString(\"ptrNameIdn\", ptrNameIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.MX:\n                        {\n                            if (record.RDATA is DnsMXRecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"preference\", rdata.Preference);\n                                jsonWriter.WriteString(\"exchange\", rdata.Exchange.Length == 0 ? \".\" : rdata.Exchange);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Exchange, out string exchangeIdn))\n                                    jsonWriter.WriteString(\"exchangeIdn\", exchangeIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TXT:\n                        {\n                            if (record.RDATA is DnsTXTRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"text\", rdata.GetText());\n                                jsonWriter.WriteBoolean(\"splitText\", rdata.CharacterStrings.Count > 1);\n\n                                jsonWriter.WriteStartArray(\"characterStrings\");\n\n                                foreach (string characterString in rdata.CharacterStrings)\n                                    jsonWriter.WriteStringValue(characterString);\n\n                                jsonWriter.WriteEndArray();\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.RP:\n                        {\n                            if (record.RDATA is DnsRPRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"mailbox\", rdata.Mailbox);\n                                jsonWriter.WriteString(\"txtDomain\", rdata.TxtDomain);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Mailbox, out string txtDomainIdn))\n                                    jsonWriter.WriteString(\"txtDomainIdn\", txtDomainIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.AAAA:\n                        {\n                            if (record.RDATA is DnsAAAARecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"ipAddress\", rdata.Address.ToString());\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SRV:\n                        {\n                            if (record.RDATA is DnsSRVRecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"priority\", rdata.Priority);\n                                jsonWriter.WriteNumber(\"weight\", rdata.Weight);\n                                jsonWriter.WriteNumber(\"port\", rdata.Port);\n                                jsonWriter.WriteString(\"target\", rdata.Target.Length == 0 ? \".\" : rdata.Target);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Target, out string targetIdn))\n                                    jsonWriter.WriteString(\"targetIdn\", targetIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NAPTR:\n                        {\n                            if (record.RDATA is DnsNAPTRRecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"order\", rdata.Order);\n                                jsonWriter.WriteNumber(\"preference\", rdata.Preference);\n                                jsonWriter.WriteString(\"flags\", rdata.Flags);\n                                jsonWriter.WriteString(\"services\", rdata.Services);\n                                jsonWriter.WriteString(\"regexp\", rdata.Regexp);\n                                jsonWriter.WriteString(\"replacement\", rdata.Replacement.Length == 0 ? \".\" : rdata.Replacement);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Replacement, out string replacementIdn))\n                                    jsonWriter.WriteString(\"replacementIdn\", replacementIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DNAME:\n                        {\n                            if (record.RDATA is DnsDNAMERecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"dname\", rdata.Domain.Length == 0 ? \".\" : rdata.Domain);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string dnameIdn))\n                                    jsonWriter.WriteString(\"dnameIdn\", dnameIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.APL:\n                        {\n                            if (record.RDATA is DnsAPLRecordData rdata)\n                            {\n                                jsonWriter.WriteStartArray(\"addressPrefixes\");\n\n                                foreach (DnsAPLRecordData.APItem apItem in rdata.APItems)\n                                {\n                                    jsonWriter.WriteStartObject();\n\n                                    jsonWriter.WriteString(\"addressFamily\", apItem.AddressFamily.ToString());\n                                    jsonWriter.WriteNumber(\"prefix\", apItem.Prefix);\n                                    jsonWriter.WriteBoolean(\"negation\", apItem.Negation);\n                                    jsonWriter.WriteString(\"afdPart\", apItem.NetworkAddress.Address.ToString());\n\n                                    jsonWriter.WriteEndObject();\n                                }\n\n                                jsonWriter.WriteEndArray();\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DS:\n                        {\n                            if (record.RDATA is DnsDSRecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"keyTag\", rdata.KeyTag);\n                                jsonWriter.WriteString(\"algorithm\", rdata.Algorithm.ToString());\n                                jsonWriter.WriteNumber(\"algorithmNumber\", (byte)rdata.Algorithm);\n                                jsonWriter.WriteString(\"digestType\", rdata.DigestType.ToString());\n                                jsonWriter.WriteNumber(\"digestTypeNumber\", (byte)rdata.DigestType);\n                                jsonWriter.WriteString(\"digest\", Convert.ToHexString(rdata.Digest));\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SSHFP:\n                        {\n                            if (record.RDATA is DnsSSHFPRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"algorithm\", rdata.Algorithm.ToString());\n                                jsonWriter.WriteString(\"fingerprintType\", rdata.FingerprintType.ToString());\n                                jsonWriter.WriteString(\"fingerprint\", Convert.ToHexString(rdata.Fingerprint));\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.RRSIG:\n                        {\n                            if (record.RDATA is DnsRRSIGRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"typeCovered\", rdata.TypeCovered.ToString());\n                                jsonWriter.WriteString(\"algorithm\", rdata.Algorithm.ToString());\n                                jsonWriter.WriteNumber(\"algorithmNumber\", (byte)rdata.Algorithm);\n                                jsonWriter.WriteNumber(\"labels\", rdata.Labels);\n                                jsonWriter.WriteNumber(\"originalTtl\", rdata.OriginalTtl);\n                                jsonWriter.WriteString(\"signatureExpiration\", DateTime.UnixEpoch.AddSeconds(rdata.SignatureExpiration));\n                                jsonWriter.WriteString(\"signatureInception\", DateTime.UnixEpoch.AddSeconds(rdata.SignatureInception));\n                                jsonWriter.WriteNumber(\"keyTag\", rdata.KeyTag);\n                                jsonWriter.WriteString(\"signersName\", rdata.SignersName.Length == 0 ? \".\" : rdata.SignersName);\n                                jsonWriter.WriteString(\"signature\", Convert.ToBase64String(rdata.Signature));\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NSEC:\n                        {\n                            if (record.RDATA is DnsNSECRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"nextDomainName\", rdata.NextDomainName);\n\n                                jsonWriter.WritePropertyName(\"types\");\n                                jsonWriter.WriteStartArray();\n\n                                foreach (DnsResourceRecordType type in rdata.Types)\n                                    jsonWriter.WriteStringValue(type.ToString());\n\n                                jsonWriter.WriteEndArray();\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DNSKEY:\n                        {\n                            if (record.RDATA is DnsDNSKEYRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"flags\", rdata.Flags.ToString());\n                                jsonWriter.WriteNumber(\"protocol\", rdata.Protocol);\n                                jsonWriter.WriteString(\"algorithm\", rdata.Algorithm.ToString());\n                                jsonWriter.WriteNumber(\"algorithmNumber\", (byte)rdata.Algorithm);\n                                jsonWriter.WriteString(\"publicKey\", rdata.PublicKey.ToString());\n                                jsonWriter.WriteNumber(\"computedKeyTag\", rdata.ComputedKeyTag);\n\n                                if (authoritativeZoneRecords)\n                                {\n                                    if ((zoneInfo is not null) && (zoneInfo.Type == AuthZoneType.Primary))\n                                    {\n                                        IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys;\n                                        if (dnssecPrivateKeys is not null)\n                                        {\n                                            foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)\n                                            {\n                                                if (dnssecPrivateKey.KeyTag == rdata.ComputedKeyTag)\n                                                {\n                                                    jsonWriter.WriteString(\"dnsKeyState\", dnssecPrivateKey.State.ToString());\n\n                                                    if (dnssecPrivateKey.State == DnssecPrivateKeyState.Published)\n                                                    {\n                                                        switch (dnssecPrivateKey.KeyType)\n                                                        {\n                                                            case DnssecPrivateKeyType.KeySigningKey:\n                                                                jsonWriter.WriteString(\"dnsKeyStateReadyBy\", dnssecPrivateKey.StateTransitionByWithDelays);\n                                                                break;\n\n                                                            case DnssecPrivateKeyType.ZoneSigningKey:\n                                                                jsonWriter.WriteString(\"dnsKeyStateActiveBy\", dnssecPrivateKey.StateTransitionByWithDelays);\n                                                                break;\n                                                        }\n                                                    }\n\n                                                    break;\n                                                }\n                                            }\n                                        }\n                                    }\n\n                                    if (rdata.Flags.HasFlag(DnsDnsKeyFlag.SecureEntryPoint))\n                                    {\n                                        jsonWriter.WritePropertyName(\"computedDigests\");\n                                        jsonWriter.WriteStartArray();\n\n                                        {\n                                            jsonWriter.WriteStartObject();\n\n                                            jsonWriter.WriteString(\"digestType\", \"SHA256\");\n                                            jsonWriter.WriteString(\"digest\", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA256).Digest));\n\n                                            jsonWriter.WriteEndObject();\n                                        }\n\n                                        {\n                                            jsonWriter.WriteStartObject();\n\n                                            jsonWriter.WriteString(\"digestType\", \"SHA384\");\n                                            jsonWriter.WriteString(\"digest\", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA384).Digest));\n\n                                            jsonWriter.WriteEndObject();\n                                        }\n\n                                        jsonWriter.WriteEndArray();\n                                    }\n                                }\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NSEC3:\n                        {\n                            if (record.RDATA is DnsNSEC3RecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"hashAlgorithm\", rdata.HashAlgorithm.ToString());\n                                jsonWriter.WriteString(\"flags\", rdata.Flags.ToString());\n                                jsonWriter.WriteNumber(\"iterations\", rdata.Iterations);\n                                jsonWriter.WriteString(\"salt\", Convert.ToHexString(rdata.Salt));\n                                jsonWriter.WriteString(\"nextHashedOwnerName\", rdata.NextHashedOwnerName);\n\n                                jsonWriter.WritePropertyName(\"types\");\n                                jsonWriter.WriteStartArray();\n\n                                foreach (DnsResourceRecordType type in rdata.Types)\n                                    jsonWriter.WriteStringValue(type.ToString());\n\n                                jsonWriter.WriteEndArray();\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NSEC3PARAM:\n                        {\n                            if (record.RDATA is DnsNSEC3PARAMRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"hashAlgorithm\", rdata.HashAlgorithm.ToString());\n                                jsonWriter.WriteString(\"flags\", rdata.Flags.ToString());\n                                jsonWriter.WriteNumber(\"iterations\", rdata.Iterations);\n                                jsonWriter.WriteString(\"salt\", Convert.ToHexString(rdata.Salt));\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TLSA:\n                        {\n                            if (record.RDATA is DnsTLSARecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"certificateUsage\", rdata.CertificateUsage.ToString().Replace('_', '-'));\n                                jsonWriter.WriteString(\"selector\", rdata.Selector.ToString());\n                                jsonWriter.WriteString(\"matchingType\", rdata.MatchingType.ToString().Replace('_', '-'));\n                                jsonWriter.WriteString(\"certificateAssociationData\", Convert.ToHexString(rdata.CertificateAssociationData));\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.ZONEMD:\n                        {\n                            if (record.RDATA is DnsZONEMDRecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"serial\", rdata.Serial);\n                                jsonWriter.WriteString(\"scheme\", rdata.Scheme.ToString());\n                                jsonWriter.WriteString(\"hashAlgorithm\", rdata.HashAlgorithm.ToString());\n                                jsonWriter.WriteString(\"digest\", Convert.ToHexString(rdata.Digest));\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SVCB:\n                    case DnsResourceRecordType.HTTPS:\n                        {\n                            if (record.RDATA is DnsSVCBRecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"svcPriority\", rdata.SvcPriority);\n                                jsonWriter.WriteString(\"svcTargetName\", rdata.TargetName);\n\n                                jsonWriter.WritePropertyName(\"svcParams\");\n                                jsonWriter.WriteStartObject();\n\n                                foreach (KeyValuePair<DnsSvcParamKey, DnsSvcParamValue> svcParam in rdata.SvcParams)\n                                    jsonWriter.WriteString(svcParam.Key.ToString().ToLowerInvariant().Replace('_', '-'), svcParam.Value.ToString());\n\n                                jsonWriter.WriteEndObject();\n\n                                if (authoritativeZoneRecords)\n                                {\n                                    SVCBRecordInfo rrInfo = record.GetAuthSVCBRecordInfo();\n\n                                    jsonWriter.WriteBoolean(\"autoIpv4Hint\", rrInfo.AutoIpv4Hint);\n                                    jsonWriter.WriteBoolean(\"autoIpv6Hint\", rrInfo.AutoIpv6Hint);\n                                }\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.URI:\n                        {\n                            if (record.RDATA is DnsURIRecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"priority\", rdata.Priority);\n                                jsonWriter.WriteNumber(\"weight\", rdata.Weight);\n                                jsonWriter.WriteString(\"uri\", rdata.Uri.AbsoluteUri);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CAA:\n                        {\n                            if (record.RDATA is DnsCAARecordData rdata)\n                            {\n                                jsonWriter.WriteNumber(\"flags\", rdata.Flags);\n                                jsonWriter.WriteString(\"tag\", rdata.Tag);\n                                jsonWriter.WriteString(\"value\", rdata.Value);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.ANAME:\n                        {\n                            if (record.RDATA is DnsANAMERecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"aname\", rdata.Domain.Length == 0 ? \".\" : rdata.Domain);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string anameIdn))\n                                    jsonWriter.WriteString(\"anameIdn\", anameIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.FWD:\n                        {\n                            if (record.RDATA is DnsForwarderRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"protocol\", rdata.Protocol.ToString());\n                                jsonWriter.WriteString(\"forwarder\", rdata.Forwarder);\n                                jsonWriter.WriteNumber(\"priority\", rdata.Priority);\n                                jsonWriter.WriteBoolean(\"dnssecValidation\", rdata.DnssecValidation);\n                                jsonWriter.WriteString(\"proxyType\", rdata.ProxyType.ToString());\n\n                                switch (rdata.ProxyType)\n                                {\n                                    case DnsForwarderRecordProxyType.Http:\n                                    case DnsForwarderRecordProxyType.Socks5:\n                                        jsonWriter.WriteString(\"proxyAddress\", rdata.ProxyAddress);\n                                        jsonWriter.WriteNumber(\"proxyPort\", rdata.ProxyPort);\n                                        jsonWriter.WriteString(\"proxyUsername\", rdata.ProxyUsername);\n                                        jsonWriter.WriteString(\"proxyPassword\", rdata.ProxyPassword);\n                                        break;\n                                }\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.APP:\n                        {\n                            if (record.RDATA is DnsApplicationRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"appName\", rdata.AppName);\n                                jsonWriter.WriteString(\"classPath\", rdata.ClassPath);\n                                jsonWriter.WriteString(\"data\", rdata.Data);\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.ALIAS:\n                        {\n                            if (record.RDATA is DnsALIASRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"type\", rdata.Type.ToString());\n                                jsonWriter.WriteString(\"alias\", rdata.Domain.Length == 0 ? \".\" : rdata.Domain);\n\n                                if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string aliasIdn))\n                                    jsonWriter.WriteString(\"aliasIdn\", aliasIdn);\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n\n                    default:\n                        {\n                            if (record.RDATA is DnsUnknownRecordData rdata)\n                            {\n                                jsonWriter.WriteString(\"value\", BitConverter.ToString(rdata.DATA).Replace('-', ':'));\n                            }\n                            else\n                            {\n                                jsonWriter.WriteString(\"dataType\", record.RDATA.GetType().Name);\n                                jsonWriter.WriteString(\"data\", record.RDATA.ToString());\n                            }\n                        }\n                        break;\n                }\n\n                jsonWriter.WriteEndObject();\n\n                jsonWriter.WriteString(\"dnssecStatus\", record.DnssecStatus.ToString());\n\n                if (authoritativeZoneRecords)\n                {\n                    GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo();\n\n                    if (authRecordInfo is NSRecordInfo nsRecordInfo)\n                    {\n                        IReadOnlyList<DnsResourceRecord> glueRecords = nsRecordInfo.GlueRecords;\n                        if (glueRecords is not null)\n                        {\n                            jsonWriter.WritePropertyName(\"glueRecords\");\n                            jsonWriter.WriteStartArray();\n\n                            foreach (DnsResourceRecord glueRecord in glueRecords)\n                                jsonWriter.WriteStringValue(glueRecord.RDATA.ToString());\n\n                            jsonWriter.WriteEndArray();\n                        }\n                    }\n\n                    jsonWriter.WriteString(\"lastUsedOn\", authRecordInfo.LastUsedOn);\n                    jsonWriter.WriteString(\"lastModified\", authRecordInfo.LastModified);\n                    jsonWriter.WriteNumber(\"expiryTtl\", authRecordInfo.ExpiryTtl);\n                    jsonWriter.WriteString(\"expiryTtlString\", ZoneFile.GetTtlString(authRecordInfo.ExpiryTtl));\n                }\n                else\n                {\n                    CacheRecordInfo cacheRecordInfo = record.GetCacheRecordInfo();\n\n                    IReadOnlyList<DnsResourceRecord> glueRecords = cacheRecordInfo.GlueRecords;\n                    if (glueRecords is not null)\n                    {\n                        jsonWriter.WritePropertyName(\"glueRecords\");\n                        jsonWriter.WriteStartArray();\n\n                        foreach (DnsResourceRecord glueRecord in glueRecords)\n                            jsonWriter.WriteStringValue(glueRecord.RDATA.ToString());\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    IReadOnlyList<DnsResourceRecord> rrsigRecords = cacheRecordInfo.RRSIGRecords;\n                    IReadOnlyList<DnsResourceRecord> nsecRecords = cacheRecordInfo.NSECRecords;\n\n                    if ((rrsigRecords is not null) || (nsecRecords is not null))\n                    {\n                        jsonWriter.WritePropertyName(\"dnssecRecords\");\n                        jsonWriter.WriteStartArray();\n\n                        if (rrsigRecords is not null)\n                        {\n                            foreach (DnsResourceRecord rrsigRecord in rrsigRecords)\n                                jsonWriter.WriteStringValue(rrsigRecord.ToString());\n                        }\n\n                        if (nsecRecords is not null)\n                        {\n                            foreach (DnsResourceRecord nsecRecord in nsecRecords)\n                                jsonWriter.WriteStringValue(nsecRecord.ToString());\n                        }\n\n                        jsonWriter.WriteEndArray();\n                    }\n\n                    NetworkAddress eDnsClientSubnet = cacheRecordInfo.EDnsClientSubnet;\n                    if (eDnsClientSubnet is not null)\n                        jsonWriter.WriteString(\"eDnsClientSubnet\", eDnsClientSubnet.ToString());\n\n                    if (record.RDATA is DnsNSRecordData nsRData)\n                    {\n                        NameServerMetadata metadata = nsRData.Metadata;\n\n                        jsonWriter.WriteStartObject(\"nameServerMetadata\");\n\n                        jsonWriter.WriteNumber(\"totalQueries\", metadata.TotalQueries);\n                        jsonWriter.WriteString(\"answerRate\", Math.Round(metadata.GetAnswerRate(), 2) + \"%\");\n                        jsonWriter.WriteString(\"smoothedRoundTripTime\", Math.Round(metadata.SRTT, 2) + \" ms\");\n                        jsonWriter.WriteString(\"smoothedPenaltyRoundTripTime\", Math.Round(metadata.SPRTT, 2) + \" ms\");\n                        jsonWriter.WriteString(\"netRoundTripTime\", Math.Round(metadata.GetNetRTT(), 2) + \" ms\");\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    DnsDatagramMetadata responseMetadata = cacheRecordInfo.ResponseMetadata;\n                    if (responseMetadata is not null)\n                    {\n                        jsonWriter.WritePropertyName(\"responseMetadata\");\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteString(\"nameServer\", responseMetadata.NameServer?.ToString());\n                        jsonWriter.WriteString(\"protocol\", (responseMetadata.NameServer is null ? DnsTransportProtocol.Udp : responseMetadata.NameServer.Protocol).ToString());\n                        jsonWriter.WriteString(\"datagramSize\", responseMetadata.DatagramSize + \" bytes\");\n                        jsonWriter.WriteString(\"roundTripTime\", Math.Round(responseMetadata.RoundTripTime, 2) + \" ms\");\n\n                        jsonWriter.WriteEndObject();\n                    }\n\n                    jsonWriter.WriteString(\"lastUsedOn\", cacheRecordInfo.LastUsedOn);\n                }\n\n                jsonWriter.WriteEndObject();\n            }\n\n            private static void WriteZoneInfoAsJson(AuthZoneInfo zoneInfo, Utf8JsonWriter jsonWriter)\n            {\n                jsonWriter.WriteStartObject();\n\n                jsonWriter.WriteString(\"name\", zoneInfo.Name);\n\n                if (DnsClient.TryConvertDomainNameToUnicode(zoneInfo.Name, out string nameIdn))\n                    jsonWriter.WriteString(\"nameIdn\", nameIdn);\n\n                jsonWriter.WriteString(\"type\", zoneInfo.Type.ToString());\n                jsonWriter.WriteString(\"lastModified\", zoneInfo.LastModified);\n                jsonWriter.WriteBoolean(\"disabled\", zoneInfo.Disabled);\n                jsonWriter.WriteNumber(\"soaSerial\", zoneInfo.ApexZone.GetZoneSoaSerial());\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                        jsonWriter.WriteBoolean(\"internal\", zoneInfo.Internal);\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Stub:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.SecondaryForwarder:\n                        jsonWriter.WriteString(\"catalog\", zoneInfo.CatalogZoneName);\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                        jsonWriter.WriteString(\"dnssecStatus\", zoneInfo.ApexZone.DnssecStatus.ToString());\n                        jsonWriter.WriteBoolean(\"hasDnssecPrivateKeys\", (zoneInfo.DnssecPrivateKeys is not null) && (zoneInfo.DnssecPrivateKeys.Count > 0));\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Secondary:\n                        jsonWriter.WriteBoolean(\"validationFailed\", zoneInfo.ValidationFailed);\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Stub:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        jsonWriter.WriteString(\"expiry\", zoneInfo.Expiry);\n                        jsonWriter.WriteBoolean(\"isExpired\", zoneInfo.IsExpired);\n                        jsonWriter.WriteBoolean(\"syncFailed\", zoneInfo.SyncFailed);\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                        if (!zoneInfo.Internal)\n                        {\n                            string[] notifyFailed = zoneInfo.NotifyFailed;\n\n                            jsonWriter.WriteBoolean(\"notifyFailed\", notifyFailed.Length > 0);\n\n                            jsonWriter.WritePropertyName(\"notifyFailedFor\");\n                            jsonWriter.WriteStartArray();\n\n                            foreach (string server in notifyFailed)\n                                jsonWriter.WriteStringValue(server);\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n                }\n\n                jsonWriter.WriteEndObject();\n            }\n\n            private static void WriteDnssecPrivateKeyAsJson(DnssecPrivateKey dnssecPrivateKey, Utf8JsonWriter jsonWriter)\n            {\n                jsonWriter.WriteStartObject();\n\n                jsonWriter.WriteNumber(\"keyTag\", dnssecPrivateKey.KeyTag);\n                jsonWriter.WriteString(\"keyType\", dnssecPrivateKey.KeyType.ToString());\n\n                switch (dnssecPrivateKey.Algorithm)\n                {\n                    case DnssecAlgorithm.RSAMD5:\n                    case DnssecAlgorithm.RSASHA1:\n                    case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:\n                    case DnssecAlgorithm.RSASHA256:\n                    case DnssecAlgorithm.RSASHA512:\n                        jsonWriter.WriteString(\"algorithm\", dnssecPrivateKey.Algorithm.ToString() + \" (\" + (dnssecPrivateKey as DnssecRsaPrivateKey).KeySize + \" bits)\");\n                        break;\n\n                    default:\n                        jsonWriter.WriteString(\"algorithm\", dnssecPrivateKey.Algorithm.ToString());\n                        break;\n                }\n\n                jsonWriter.WriteNumber(\"algorithmNumber\", (byte)dnssecPrivateKey.Algorithm);\n\n                jsonWriter.WriteString(\"state\", dnssecPrivateKey.State.ToString());\n                jsonWriter.WriteString(\"stateChangedOn\", dnssecPrivateKey.StateChangedOn);\n\n                if (dnssecPrivateKey.State == DnssecPrivateKeyState.Published)\n                {\n                    switch (dnssecPrivateKey.KeyType)\n                    {\n                        case DnssecPrivateKeyType.KeySigningKey:\n                            jsonWriter.WriteString(\"stateReadyBy\", dnssecPrivateKey.StateTransitionByWithDelays);\n                            break;\n\n                        case DnssecPrivateKeyType.ZoneSigningKey:\n                            jsonWriter.WriteString(\"stateActiveBy\", dnssecPrivateKey.StateTransitionByWithDelays);\n                            break;\n                    }\n                }\n\n                jsonWriter.WriteBoolean(\"isRetiring\", dnssecPrivateKey.IsRetiring);\n                jsonWriter.WriteNumber(\"rolloverDays\", dnssecPrivateKey.RolloverDays);\n\n                jsonWriter.WriteEndObject();\n            }\n\n            private static string[] DecodeCharacterStrings(string text)\n            {\n                string[] characterStrings = text.Split(_newLineSeparator, StringSplitOptions.RemoveEmptyEntries);\n\n                for (int i = 0; i < characterStrings.Length; i++)\n                    characterStrings[i] = Unescape(characterStrings[i]);\n\n                return characterStrings;\n            }\n\n            private static string Unescape(string text)\n            {\n                StringBuilder sb = new StringBuilder(text.Length);\n\n                for (int i = 0, j; i < text.Length; i++)\n                {\n                    char c = text[i];\n                    if (c == '\\\\')\n                    {\n                        j = i + 1;\n\n                        if (j == text.Length)\n                        {\n                            sb.Append(c);\n                            break;\n                        }\n\n                        char next = text[j];\n                        switch (next)\n                        {\n                            case 'n':\n                                sb.Append('\\n');\n                                break;\n\n                            case 'r':\n                                sb.Append('\\r');\n                                break;\n\n                            case 't':\n                                sb.Append('\\t');\n                                break;\n\n                            case '\\\\':\n                                sb.Append('\\\\');\n                                break;\n\n                            default:\n                                sb.Append(c).Append(next);\n                                break;\n                        }\n\n                        i++;\n                    }\n                    else\n                    {\n                        sb.Append(c);\n                    }\n                }\n\n                return sb.ToString();\n            }\n\n            private static string GetSvcbTargetName(DnsResourceRecord svcbRecord)\n            {\n                DnsSVCBRecordData rData = svcbRecord.RDATA as DnsSVCBRecordData;\n\n                if (rData.TargetName.Length > 0)\n                    return rData.TargetName;\n\n                if (rData.SvcPriority == 0) //alias mode\n                    return null;\n\n                //service mode\n                return svcbRecord.Name;\n            }\n\n            private void ResolveSvcbAutoHints(string zoneName, DnsResourceRecord svcbRecord, bool resolveIpv4Hint, bool resolveIpv6Hint, Dictionary<DnsSvcParamKey, DnsSvcParamValue> svcParams, IReadOnlyCollection<DnsResourceRecord> importRecords = null)\n            {\n                string targetName = GetSvcbTargetName(svcbRecord);\n                if (targetName is not null)\n                    ResolveSvcbAutoHints(zoneName, targetName, resolveIpv4Hint, resolveIpv6Hint, svcParams, importRecords);\n            }\n\n            private void ResolveSvcbAutoHints(string zoneName, string targetName, bool resolveIpv4Hint, bool resolveIpv6Hint, Dictionary<DnsSvcParamKey, DnsSvcParamValue> svcParams, IReadOnlyCollection<DnsResourceRecord> importRecords = null)\n            {\n                if (resolveIpv4Hint)\n                {\n                    List<IPAddress> ipv4Hint = new List<IPAddress>();\n\n                    IReadOnlyList<DnsResourceRecord> records = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneName, targetName, DnsResourceRecordType.A);\n\n                    foreach (DnsResourceRecord record in records)\n                    {\n                        if (record.GetAuthGenericRecordInfo().Disabled)\n                            continue;\n\n                        ipv4Hint.Add((record.RDATA as DnsARecordData).Address);\n                    }\n\n                    if (importRecords is not null)\n                    {\n                        foreach (DnsResourceRecord record in importRecords)\n                        {\n                            if (record.Type != DnsResourceRecordType.A)\n                                continue;\n\n                            if (record.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase))\n                            {\n                                IPAddress address = (record.RDATA as DnsARecordData).Address;\n\n                                if (!ipv4Hint.Contains(address))\n                                    ipv4Hint.Add(address);\n                            }\n                        }\n                    }\n\n                    if (ipv4Hint.Count > 0)\n                        svcParams[DnsSvcParamKey.IPv4Hint] = new DnsSvcIPv4HintParamValue(ipv4Hint);\n                    else\n                        svcParams.Remove(DnsSvcParamKey.IPv4Hint);\n                }\n\n                if (resolveIpv6Hint)\n                {\n                    List<IPAddress> ipv6Hint = new List<IPAddress>();\n\n                    IReadOnlyList<DnsResourceRecord> records = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneName, targetName, DnsResourceRecordType.AAAA);\n\n                    foreach (DnsResourceRecord record in records)\n                    {\n                        if (record.GetAuthGenericRecordInfo().Disabled)\n                            continue;\n\n                        ipv6Hint.Add((record.RDATA as DnsAAAARecordData).Address);\n                    }\n\n                    if (importRecords is not null)\n                    {\n                        foreach (DnsResourceRecord record in importRecords)\n                        {\n                            if (record.Type != DnsResourceRecordType.AAAA)\n                                continue;\n\n                            if (record.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase))\n                            {\n                                IPAddress address = (record.RDATA as DnsAAAARecordData).Address;\n\n                                if (!ipv6Hint.Contains(address))\n                                    ipv6Hint.Add(address);\n                            }\n                        }\n                    }\n\n                    if (ipv6Hint.Count > 0)\n                        svcParams[DnsSvcParamKey.IPv6Hint] = new DnsSvcIPv6HintParamValue(ipv6Hint);\n                    else\n                        svcParams.Remove(DnsSvcParamKey.IPv6Hint);\n                }\n            }\n\n            private void UpdateSvcbAutoHints(string zoneName, string targetName, bool resolveIpv4Hint, bool resolveIpv6Hint)\n            {\n                List<DnsResourceRecord> allSvcbRecords = new List<DnsResourceRecord>();\n                _dnsWebService._dnsServer.AuthZoneManager.ListAllZoneRecords(zoneName, [DnsResourceRecordType.SVCB, DnsResourceRecordType.HTTPS], allSvcbRecords);\n\n                foreach (DnsResourceRecord record in allSvcbRecords)\n                {\n                    SVCBRecordInfo info = record.GetAuthSVCBRecordInfo();\n                    if ((info.AutoIpv4Hint && resolveIpv4Hint) || (info.AutoIpv6Hint && resolveIpv6Hint))\n                    {\n                        string scvbTargetName = GetSvcbTargetName(record);\n                        if (targetName.Equals(scvbTargetName, StringComparison.OrdinalIgnoreCase))\n                        {\n                            DnsSVCBRecordData oldRData = record.RDATA as DnsSVCBRecordData;\n\n                            Dictionary<DnsSvcParamKey, DnsSvcParamValue> newSvcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(oldRData.SvcParams);\n                            ResolveSvcbAutoHints(zoneName, targetName, resolveIpv4Hint, resolveIpv6Hint, newSvcParams);\n\n                            DnsSVCBRecordData newRData = new DnsSVCBRecordData(oldRData.SvcPriority, oldRData.TargetName, newSvcParams);\n                            DnsResourceRecord newRecord = new DnsResourceRecord(record.Name, record.Type, record.Class, record.TTL, newRData) { Tag = record.Tag };\n\n                            _dnsWebService._dnsServer.AuthZoneManager.UpdateRecord(zoneName, record, newRecord);\n                        }\n                    }\n                }\n            }\n\n            private async Task<List<DnsResourceRecord>> ReadRecordsToImportFromAsync(string zoneName, AuthZoneType zoneType, string catalogZoneName, bool overwrite, TextReader zoneFile)\n            {\n                List<DnsResourceRecord> records = await ZoneFile.ReadZoneFileFromAsync(zoneFile, zoneName, _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl);\n                List<DnsResourceRecord> newRecords = new List<DnsResourceRecord>(records.Count);\n\n                foreach (DnsResourceRecord record in records)\n                {\n                    if (record.Class != DnsClass.IN)\n                        throw new DnsWebServiceException(\"Cannot import records: only IN class is supported by the DNS server.\");\n\n                    if (!AuthZoneManager.DomainBelongsToZone(zoneName, record.Name))\n                    {\n                        switch (record.Type)\n                        {\n                            case DnsResourceRecordType.A:\n                            case DnsResourceRecordType.AAAA:\n                                continue; //glue records\n\n                            default:\n                                throw new DnsServerException(\"Cannot import records: the domain name '\" + record.Name + \"' does not belong to the zone '\" + zoneName + \"'.\");\n                        }\n                    }\n\n                    bool disabled = false;\n                    string comments = null;\n\n                    if (record.Tag is string tagValue)\n                    {\n                        if (tagValue.TrimStart().StartsWith('{'))\n                        {\n                            try\n                            {\n                                using JsonDocument jsonDocument = JsonDocument.Parse(tagValue);\n                                JsonElement json = jsonDocument.RootElement;\n\n                                if (json.TryGetProperty(\"disabled\", out JsonElement jsonDisabled))\n                                    disabled = jsonDisabled.ValueKind == JsonValueKind.True;\n\n                                if (json.TryGetProperty(\"comments\", out JsonElement jsonComments) && (jsonComments.ValueKind == JsonValueKind.String))\n                                    comments = jsonComments.GetString();\n                            }\n                            catch\n                            {\n                                comments = tagValue.Replace(\"\\\\r\", \"\").Replace(\"\\\\n\", \"\\n\");\n                            }\n                        }\n                        else\n                        {\n                            comments = tagValue.Replace(\"\\\\r\", \"\").Replace(\"\\\\n\", \"\\n\");\n                        }\n                    }\n\n                    switch (record.Type)\n                    {\n                        case DnsResourceRecordType.DNSKEY:\n                        case DnsResourceRecordType.RRSIG:\n                        case DnsResourceRecordType.NSEC:\n                        case DnsResourceRecordType.NSEC3:\n                        case DnsResourceRecordType.NSEC3PARAM:\n                            continue; //skip DNSSEC records\n\n                        case DnsResourceRecordType.NS:\n                            {\n                                if (record.Tag is string)\n                                {\n                                    NSRecordInfo rrInfo = new NSRecordInfo();\n\n                                    rrInfo.Disabled = disabled;\n                                    rrInfo.Comments = comments;\n\n                                    record.Tag = rrInfo;\n                                }\n\n                                record.SyncGlueRecords(records);\n\n                                newRecords.Add(record);\n                            }\n                            break;\n\n                        case DnsResourceRecordType.SOA:\n                            {\n                                if (record.Tag is string)\n                                {\n                                    SOARecordInfo rrInfo = new SOARecordInfo();\n                                    rrInfo.Comments = comments;\n\n                                    record.Tag = rrInfo;\n                                }\n\n                                newRecords.Add(record);\n                            }\n                            break;\n\n                        case DnsResourceRecordType.SVCB:\n                        case DnsResourceRecordType.HTTPS:\n                            {\n                                if (record.Tag is string)\n                                {\n                                    SVCBRecordInfo rrInfo = new SVCBRecordInfo();\n\n                                    rrInfo.Disabled = disabled;\n                                    rrInfo.Comments = comments;\n\n                                    record.Tag = rrInfo;\n                                }\n\n                                if (record.RDATA is DnsSVCBRecordData rdata && (rdata.AutoIpv4Hint || rdata.AutoIpv6Hint))\n                                {\n                                    if (rdata.AutoIpv4Hint)\n                                        record.GetAuthSVCBRecordInfo().AutoIpv4Hint = true;\n\n                                    if (rdata.AutoIpv6Hint)\n                                        record.GetAuthSVCBRecordInfo().AutoIpv6Hint = true;\n\n                                    Dictionary<DnsSvcParamKey, DnsSvcParamValue> svcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(rdata.SvcParams);\n                                    DnsResourceRecord newRecord = new DnsResourceRecord(record.Name, record.Type, record.Class, record.TTL, new DnsSVCBRecordData(rdata.SvcPriority, rdata.TargetName, svcParams)) { Tag = record.Tag };\n\n                                    ResolveSvcbAutoHints(zoneName, record, rdata.AutoIpv4Hint, rdata.AutoIpv6Hint, svcParams, records);\n\n                                    newRecords.Add(newRecord);\n                                    break;\n                                }\n\n                                newRecords.Add(record);\n                            }\n                            break;\n\n                        default:\n                            {\n                                if (record.Tag is string)\n                                {\n                                    GenericRecordInfo rrInfo = new GenericRecordInfo();\n\n                                    rrInfo.Disabled = disabled;\n                                    rrInfo.Comments = comments;\n\n                                    record.Tag = rrInfo;\n                                }\n\n                                newRecords.Add(record);\n                            }\n                            break;\n                    }\n                }\n\n                //validate records\n                if ((zoneType == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneName))\n                {\n                    int nsCount = 0;\n\n                    foreach (DnsResourceRecord newRecord in newRecords)\n                    {\n                        switch (newRecord.Type)\n                        {\n                            case DnsResourceRecordType.NS:\n                                if (zoneName.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase))\n                                {\n                                    NSRecordInfo recordInfo = newRecord.GetAuthNSRecordInfo();\n\n                                    if (recordInfo.Disabled)\n                                        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.\");\n\n                                    if (recordInfo.GlueRecords is not null)\n                                        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.\");\n\n                                    string nsDomain = (newRecord.RDATA as DnsNSRecordData).NameServer;\n                                    bool found = false;\n\n                                    foreach (KeyValuePair<int, ClusterNode> clusterNode in _dnsWebService._clusterManager.ClusterNodes)\n                                    {\n                                        if (nsDomain.Equals(clusterNode.Value.Name, StringComparison.OrdinalIgnoreCase))\n                                        {\n                                            found = true;\n                                            break;\n                                        }\n                                    }\n\n                                    if (!found)\n                                        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.\");\n                                }\n\n                                nsCount++;\n                                break;\n\n                            case DnsResourceRecordType.SOA:\n                                DnsSOARecordData soa = newRecord.RDATA as DnsSOARecordData;\n\n                                if (!soa.PrimaryNameServer.Equals(_dnsWebService._dnsServer.ServerDomain, StringComparison.OrdinalIgnoreCase))\n                                    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.\");\n\n                                break;\n                        }\n                    }\n\n                    if (overwrite)\n                    {\n                        if ((nsCount > 0) && (nsCount != _dnsWebService._clusterManager.ClusterNodes.Count)) //check attempt to replace NS records\n                            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.\");\n                    }\n                    else\n                    {\n                        if (nsCount > 0) //check attempt to add NS records\n                            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.\");\n                    }\n                }\n\n                return newRecords;\n            }\n\n            #endregion\n\n            #region public\n\n            public void ListZones(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                IReadOnlyList<AuthZoneInfo> zoneInfoList = _dnsWebService._dnsServer.AuthZoneManager.GetZones(delegate (AuthZoneInfo zoneInfo)\n                {\n                    return _dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View);\n                });\n\n                if (request.TryGetQueryOrForm(\"pageNumber\", int.Parse, out int pageNumber))\n                {\n                    int zonesPerPage = request.GetQueryOrForm(\"zonesPerPage\", int.Parse, 10);\n                    int totalPages;\n                    int totalZones = zoneInfoList.Count;\n\n                    if (totalZones > 0)\n                    {\n                        if (pageNumber == 0)\n                            pageNumber = 1;\n\n                        totalPages = (totalZones / zonesPerPage) + (totalZones % zonesPerPage > 0 ? 1 : 0);\n\n                        if ((pageNumber > totalPages) || (pageNumber < 0))\n                            pageNumber = totalPages;\n\n                        int start = (pageNumber - 1) * zonesPerPage;\n                        int end = Math.Min(start + zonesPerPage, totalZones);\n\n                        List<AuthZoneInfo> zoneInfoPageList = new List<AuthZoneInfo>(end - start);\n\n                        for (int i = start; i < end; i++)\n                            zoneInfoPageList.Add(zoneInfoList[i]);\n\n                        zoneInfoList = zoneInfoPageList;\n                    }\n                    else\n                    {\n                        pageNumber = 0;\n                        totalPages = 0;\n                    }\n\n                    jsonWriter.WriteNumber(\"pageNumber\", pageNumber);\n                    jsonWriter.WriteNumber(\"totalPages\", totalPages);\n                    jsonWriter.WriteNumber(\"totalZones\", totalZones);\n                }\n\n                jsonWriter.WritePropertyName(\"zones\");\n                jsonWriter.WriteStartArray();\n\n                foreach (AuthZoneInfo zoneInfo in zoneInfoList)\n                    WriteZoneInfoAsJson(zoneInfo, jsonWriter);\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void ListCatalogZones(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                IReadOnlyList<AuthZoneInfo> catalogZoneInfoList = _dnsWebService._dnsServer.AuthZoneManager.GetCatalogZones(delegate (AuthZoneInfo catalogZoneInfo)\n                {\n                    return !catalogZoneInfo.Disabled && _dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify);\n                });\n\n                jsonWriter.WritePropertyName(\"catalogZoneNames\");\n                jsonWriter.WriteStartArray();\n\n                foreach (AuthZoneInfo catalogZoneInfo in catalogZoneInfoList)\n                    jsonWriter.WriteStringValue(catalogZoneInfo.Name);\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public async Task CreateZoneAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrFormAlt(\"zone\", \"domain\");\n\n                if (IPAddress.TryParse(zoneName, out IPAddress ipAddress))\n                {\n                    zoneName = ipAddress.GetReverseDomain().ToLowerInvariant();\n                }\n                else\n                {\n                    if (zoneName.Contains('/'))\n                    {\n                        string[] parts = zoneName.Split('/');\n                        if ((parts.Length == 2) && IPAddress.TryParse(parts[0], out ipAddress) && int.TryParse(parts[1], out int subnetMaskWidth))\n                            zoneName = Zone.GetReverseZone(ipAddress, subnetMaskWidth);\n                    }\n                    else\n                    {\n                        zoneName = zoneName.Trim('.');\n                    }\n\n                    if (zoneName.Contains('*'))\n                        throw new DnsWebServiceException(\"Domain name for a zone cannot contain wildcard character.\");\n\n                    foreach (char invalidChar in Path.GetInvalidFileNameChars())\n                    {\n                        if (zoneName.Contains(invalidChar))\n                            throw new DnsWebServiceException(\"The zone name contains an invalid character: \" + invalidChar);\n                    }\n\n                    if (DnsClient.IsDomainNameUnicode(zoneName))\n                        zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n                }\n\n                AuthZoneType type = request.GetQueryOrFormEnum(\"type\", AuthZoneType.Primary);\n                string catalogZoneName = request.GetQueryOrForm(\"catalog\", null);\n\n                //read records to import, if any\n                List<DnsResourceRecord> importRecords = null;\n\n                switch (type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Forwarder:\n                        if (request.HasFormContentType && (request.Form.Files.Count > 0))\n                        {\n                            using (TextReader zoneFile = new StreamReader(request.Form.Files[0].OpenReadStream()))\n                            {\n                                importRecords = await ReadRecordsToImportFromAsync(zoneName, type, catalogZoneName, false, zoneFile);\n                            }\n                        }\n\n                        break;\n                }\n\n                //create zone\n                AuthZoneInfo zoneInfo;\n\n                switch (type)\n                {\n                    case AuthZoneType.Primary:\n                        {\n                            bool useSoaSerialDateScheme = request.GetQueryOrForm(\"useSoaSerialDateScheme\", bool.Parse, _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme);\n\n                            AuthZoneInfo catalogZoneInfo = null;\n\n                            if (catalogZoneName is not null)\n                            {\n                                catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName);\n                                if (catalogZoneInfo is null)\n                                    throw new DnsWebServiceException(\"No such Catalog zone was found: \" + catalogZoneName);\n\n                                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                                    throw new DnsWebServiceException(\"Access was denied to use Catalog zone: \" + catalogZoneInfo.Name);\n                            }\n\n                            zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(zoneName, useSoaSerialDateScheme);\n                            if (zoneInfo is null)\n                                throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n\n                            //set permissions\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SaveConfigFile();\n\n                            //add membership for catalog zone\n                            if (catalogZoneInfo is not null)\n                            {\n                                _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo);\n\n                                if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneInfo.Name))\n                                    _dnsWebService._clusterManager.UpdateClusterRecordsFor(zoneInfo);\n                            }\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Authoritative Primary zone was created: \" + zoneInfo.DisplayName);\n                        }\n                        break;\n\n                    case AuthZoneType.Secondary:\n                        {\n                            string primaryNameServerAddresses = request.GetQueryOrForm(\"primaryNameServerAddresses\", null);\n                            DnsTransportProtocol primaryZoneTransferProtocol = request.GetQueryOrFormEnum(\"zoneTransferProtocol\", DnsTransportProtocol.Tcp);\n                            string primaryZoneTransferTsigKeyName = request.GetQueryOrForm(\"tsigKeyName\", null);\n                            bool validateZone = request.GetQueryOrForm(\"validateZone\", bool.Parse, false);\n\n                            if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic)\n                                DnsWebService.ValidateQuicSupport();\n\n                            AuthZoneInfo catalogZoneInfo = null;\n\n                            if (catalogZoneName is not null)\n                            {\n                                catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName);\n                                if (catalogZoneInfo is null)\n                                    throw new DnsWebServiceException(\"No such Catalog zone was found: \" + catalogZoneName);\n\n                                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                                    throw new DnsWebServiceException(\"Access was denied to use Catalog zone: \" + catalogZoneInfo.Name);\n                            }\n\n                            zoneInfo = await _dnsWebService._dnsServer.AuthZoneManager.CreateSecondaryZoneAsync(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone);\n                            if (zoneInfo is null)\n                                throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n\n                            //set permissions\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SaveConfigFile();\n\n                            //add membership for catalog zone\n                            if (catalogZoneInfo is not null)\n                            {\n                                _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo);\n\n                                zoneInfo.OverrideCatalogPrimaryNameServers = true; //always true for secondary member zones\n                            }\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Authoritative Secondary zone was created: \" + zoneInfo.DisplayName);\n                        }\n                        break;\n\n                    case AuthZoneType.Stub:\n                        {\n                            string primaryNameServerAddresses = request.GetQueryOrForm(\"primaryNameServerAddresses\", null);\n\n                            AuthZoneInfo catalogZoneInfo = null;\n\n                            if (catalogZoneName is not null)\n                            {\n                                catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName);\n                                if (catalogZoneInfo is null)\n                                    throw new DnsWebServiceException(\"No such Catalog zone was found: \" + catalogZoneName);\n\n                                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                                    throw new DnsWebServiceException(\"Access was denied to use Catalog zone: \" + catalogZoneInfo.Name);\n                            }\n\n                            zoneInfo = await _dnsWebService._dnsServer.AuthZoneManager.CreateStubZoneAsync(zoneName, primaryNameServerAddresses);\n                            if (zoneInfo is null)\n                                throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n\n                            //set permissions\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SaveConfigFile();\n\n                            //add membership for catalog zone\n                            if (catalogZoneInfo is not null)\n                                _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo);\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Stub zone was created: \" + zoneInfo.DisplayName);\n                        }\n                        break;\n\n                    case AuthZoneType.Forwarder:\n                        {\n                            bool initializeForwarder = request.GetQueryOrForm(\"initializeForwarder\", bool.Parse, true);\n\n                            AuthZoneInfo catalogZoneInfo = null;\n\n                            if (catalogZoneName is not null)\n                            {\n                                catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName);\n                                if (catalogZoneInfo is null)\n                                    throw new DnsWebServiceException(\"No such Catalog zone was found: \" + catalogZoneName);\n\n                                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                                    throw new DnsWebServiceException(\"Access was denied to use Catalog zone: \" + catalogZoneInfo.Name);\n                            }\n\n                            if (initializeForwarder)\n                            {\n                                DnsTransportProtocol forwarderProtocol = request.GetQueryOrFormEnum(\"protocol\", DnsTransportProtocol.Udp);\n                                string forwarder = request.GetQueryOrForm(\"forwarder\");\n                                bool dnssecValidation = request.GetQueryOrForm(\"dnssecValidation\", bool.Parse, false);\n                                DnsForwarderRecordProxyType proxyType = request.GetQueryOrFormEnum(\"proxyType\", DnsForwarderRecordProxyType.DefaultProxy);\n\n                                string proxyAddress = null;\n                                ushort proxyPort = 0;\n                                string proxyUsername = null;\n                                string proxyPassword = null;\n\n                                switch (proxyType)\n                                {\n                                    case DnsForwarderRecordProxyType.Http:\n                                    case DnsForwarderRecordProxyType.Socks5:\n                                        proxyAddress = request.GetQueryOrForm(\"proxyAddress\");\n                                        proxyPort = request.GetQueryOrForm(\"proxyPort\", ushort.Parse);\n                                        proxyUsername = request.QueryOrForm(\"proxyUsername\");\n                                        proxyPassword = request.QueryOrForm(\"proxyPassword\");\n                                        break;\n                                }\n\n                                if (forwarderProtocol == DnsTransportProtocol.Quic)\n                                    DnsWebService.ValidateQuicSupport();\n\n                                zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateForwarderZone(zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, null);\n                                if (zoneInfo is null)\n                                    throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n                            }\n                            else\n                            {\n                                zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateForwarderZone(zoneName);\n                                if (zoneInfo is null)\n                                    throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n                            }\n\n                            //set permissions\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SaveConfigFile();\n\n                            //add membership for catalog zone\n                            if (catalogZoneInfo is not null)\n                                _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo);\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Forwarder zone was created: \" + zoneInfo.DisplayName);\n                        }\n                        break;\n\n                    case AuthZoneType.SecondaryForwarder:\n                        {\n                            string primaryNameServerAddresses = request.GetQueryOrForm(\"primaryNameServerAddresses\");\n                            DnsTransportProtocol primaryZoneTransferProtocol = request.GetQueryOrFormEnum(\"zoneTransferProtocol\", DnsTransportProtocol.Tcp);\n                            string primaryZoneTransferTsigKeyName = request.GetQueryOrForm(\"tsigKeyName\", null);\n\n                            if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic)\n                                DnsWebService.ValidateQuicSupport();\n\n                            zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateSecondaryForwarderZone(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName);\n                            if (zoneInfo is null)\n                                throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n\n                            //set permissions\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SaveConfigFile();\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Secondary Forwarder zone was created: \" + zoneInfo.DisplayName);\n                        }\n                        break;\n\n                    case AuthZoneType.Catalog:\n                        {\n                            zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateCatalogZone(zoneName);\n                            if (zoneInfo is null)\n                                throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n\n                            //set permissions\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SaveConfigFile();\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Catalog zone was created: \" + zoneInfo.DisplayName);\n                        }\n                        break;\n\n                    case AuthZoneType.SecondaryCatalog:\n                        {\n                            string primaryNameServerAddresses = request.GetQueryOrForm(\"primaryNameServerAddresses\");\n                            DnsTransportProtocol primaryZoneTransferProtocol = request.GetQueryOrFormEnum(\"zoneTransferProtocol\", DnsTransportProtocol.Tcp);\n                            string primaryZoneTransferTsigKeyName = request.GetQueryOrForm(\"tsigKeyName\", null);\n\n                            if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic)\n                                DnsWebService.ValidateQuicSupport();\n\n                            zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateSecondaryCatalogZone(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName);\n                            if (zoneInfo is null)\n                                throw new DnsWebServiceException(\"Zone already exists: \" + zoneName);\n\n                            //set permissions\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                            _dnsWebService._authManager.SaveConfigFile();\n\n                            _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Secondary Catalog zone was created: \" + zoneInfo.DisplayName);\n                        }\n                        break;\n\n                    default:\n                        throw new NotSupportedException(\"Zone type not supported.\");\n                }\n\n                //delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zones\n                _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name);\n\n                //import records, if any\n                if (importRecords is not null)\n                {\n                    //delete existing NS/FWD record \n                    switch (type)\n                    {\n                        case AuthZoneType.Primary:\n                            _dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, zoneInfo.Name, DnsResourceRecordType.NS);\n                            break;\n\n                        case AuthZoneType.Forwarder:\n                            _dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, zoneInfo.Name, DnsResourceRecordType.FWD);\n                            break;\n                    }\n\n                    //import records\n                    _dnsWebService._dnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, importRecords, false, false);\n                }\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n                jsonWriter.WriteString(\"domain\", string.IsNullOrEmpty(zoneInfo.Name) ? \".\" : zoneInfo.Name);\n            }\n\n            public async Task ImportZoneAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                bool overwrite = request.GetQueryOrForm(\"overwrite\", bool.Parse, true);\n                bool overwriteSoaSerial = request.GetQueryOrForm(\"overwriteSoaSerial\", bool.Parse, false);\n\n                TextReader textReader;\n\n                switch (request.ContentType?.ToLowerInvariant())\n                {\n                    case \"application/x-www-form-urlencoded\":\n                        string zoneRecords = request.GetQueryOrForm(\"records\");\n                        textReader = new StringReader(zoneRecords);\n                        break;\n\n                    case \"text/plain\":\n                        textReader = new StreamReader(request.Body);\n                        break;\n\n                    default:\n                        if (!request.HasFormContentType || (request.Form.Files.Count == 0))\n                            throw new DnsWebServiceException(\"The zone file to import is missing.\");\n\n                        textReader = new StreamReader(request.Form.Files[0].OpenReadStream());\n                        break;\n                }\n\n                List<DnsResourceRecord> records;\n\n                using (TextReader zoneFile = textReader)\n                {\n                    records = await ReadRecordsToImportFromAsync(zoneInfo.Name, zoneInfo.Type, zoneInfo.CatalogZoneName, overwrite, zoneFile);\n                }\n\n                _dnsWebService._dnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, records, overwrite, overwriteSoaSerial);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Total \" + records.Count + \" record(s) were imported successfully into \" + zoneInfo.TypeName + \" zone: \" + zoneInfo.DisplayName);\n            }\n\n            public async Task ExportZoneAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                List<DnsResourceRecord> records = new List<DnsResourceRecord>();\n\n                _dnsWebService._dnsServer.AuthZoneManager.ListAllZoneRecords(zoneInfo.Name, records);\n\n                foreach (DnsResourceRecord record in records)\n                {\n                    switch (record.Type)\n                    {\n                        case DnsResourceRecordType.SVCB:\n                        case DnsResourceRecordType.HTTPS:\n                            SVCBRecordInfo info = record.GetAuthSVCBRecordInfo();\n\n                            if (info.AutoIpv4Hint)\n                                (record.RDATA as DnsSVCBRecordData).AutoIpv4Hint = true;\n\n                            if (info.AutoIpv6Hint)\n                                (record.RDATA as DnsSVCBRecordData).AutoIpv6Hint = true;\n\n                            break;\n                    }\n                }\n\n                HttpResponse response = context.Response;\n\n                response.ContentType = \"text/plain\";\n                response.Headers.ContentDisposition = \"attachment;filename=\" + (zoneInfo.Name.Length == 0 ? \"root.zone\" : zoneInfo.Name + \".zone\");\n\n                await using (StreamWriter sW = new StreamWriter(response.Body))\n                {\n                    await ZoneFile.WriteZoneFileToAsync(sW, zoneInfo.Name, records, delegate (DnsResourceRecord record)\n                    {\n                        if (record.Tag is null)\n                            return null;\n\n                        GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();\n\n                        if (recordInfo.Disabled || ((recordInfo.Comments is not null) && recordInfo.Comments.TrimStart().StartsWith('{')))\n                        {\n                            using (MemoryStream mS = new MemoryStream())\n                            {\n                                Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS);\n\n                                jsonWriter.WriteStartObject();\n                                jsonWriter.WriteBoolean(\"disabled\", recordInfo.Disabled);\n                                jsonWriter.WriteString(\"comments\", recordInfo.Comments);\n                                jsonWriter.WriteEndObject();\n\n                                jsonWriter.Flush();\n\n                                return Encoding.UTF8.GetString(mS.ToArray());\n                            }\n                        }\n\n                        return recordInfo.Comments?.Replace(\"\\r\", \"\").Replace(\"\\n\", \"\\\\n\");\n                    });\n                }\n            }\n\n            public void CloneZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                string sourceZoneName = request.GetQueryOrForm(\"sourceZone\").Trim('.');\n                if (DnsClient.IsDomainNameUnicode(sourceZoneName))\n                    sourceZoneName = DnsClient.ConvertDomainNameToAscii(sourceZoneName);\n\n                AuthZoneInfo sourceZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(sourceZoneName);\n                if (sourceZoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + sourceZoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sourceZoneInfo.Name, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CloneZone(zoneName, sourceZoneInfo.Name);\n\n                //clone user/group permissions from source zone\n                Permission sourceZonePermissions = _dnsWebService._authManager.GetPermission(PermissionSection.Zones, sourceZoneInfo.Name);\n\n                foreach (KeyValuePair<User, PermissionFlag> userPermission in sourceZonePermissions.UserPermissions)\n                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, userPermission.Key, userPermission.Value);\n\n                foreach (KeyValuePair<Group, PermissionFlag> groupPermissions in sourceZonePermissions.GroupPermissions)\n                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, groupPermissions.Key, groupPermissions.Value);\n\n                //set default permissions\n                _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                _dnsWebService._authManager.SaveConfigFile();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] \" + sourceZoneInfo.TypeName + \" zone '\" + sourceZoneInfo.DisplayName + \"' was cloned as '\" + zoneInfo.DisplayName + \"' sucessfully.\");\n            }\n\n            public void ConvertZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n                AuthZoneType type = request.GetQueryOrFormEnum<AuthZoneType>(\"type\");\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterPrimaryZone(zoneInfo.Name))\n                    throw new DnsWebServiceException(\"Cannot convert the Cluster Primary zone '\" + zoneInfo.DisplayName + \"'.\");\n\n                _dnsWebService._dnsServer.AuthZoneManager.ConvertZoneTypeTo(zoneInfo.Name, type);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] \" + zoneInfo.TypeName + \" zone '\" + zoneInfo.DisplayName + \"' was converted to \" + AuthZoneInfo.GetZoneTypeName(type) + \" zone sucessfully.\");\n            }\n\n            public void SignPrimaryZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string algorithm = request.GetQueryOrForm(\"algorithm\");\n                string pemKskPrivateKey = request.GetQueryOrForm(\"pemKskPrivateKey\", null);\n                string pemZskPrivateKey = request.GetQueryOrForm(\"pemZskPrivateKey\", null);\n                uint dnsKeyTtl = request.GetQueryOrForm(\"dnsKeyTtl\", ZoneFile.ParseTtl, 3600u);\n                ushort zskRolloverDays = request.GetQueryOrForm(\"zskRolloverDays\", ushort.Parse, Convert.ToUInt16(pemZskPrivateKey is null ? 30 : 0));\n\n                bool useNSEC3 = false;\n                string strNxProof = request.QueryOrForm(\"nxProof\");\n                if (!string.IsNullOrEmpty(strNxProof))\n                {\n                    switch (strNxProof.ToUpper())\n                    {\n                        case \"NSEC\":\n                            useNSEC3 = false;\n                            break;\n\n                        case \"NSEC3\":\n                            useNSEC3 = true;\n                            break;\n\n                        default:\n                            throw new NotSupportedException(\"Non-existence proof type is not supported: \" + strNxProof);\n                    }\n                }\n\n                ushort iterations = 0;\n                byte saltLength = 0;\n\n                if (useNSEC3)\n                {\n                    iterations = request.GetQueryOrForm<ushort>(\"iterations\", ushort.Parse, 0);\n                    saltLength = request.GetQueryOrForm<byte>(\"saltLength\", byte.Parse, 0);\n                }\n\n                DnssecPrivateKey kskPrivateKey;\n                DnssecPrivateKey zskPrivateKey;\n\n                switch (algorithm.ToUpper())\n                {\n                    case \"RSA\":\n                        {\n                            string hashAlgorithm = request.GetQueryOrForm(\"hashAlgorithm\");\n\n                            DnssecAlgorithm dnssecAlgorithm;\n\n                            switch (hashAlgorithm.ToUpper())\n                            {\n                                case \"MD5\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSAMD5;\n                                    break;\n\n                                case \"SHA1\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSASHA1;\n                                    break;\n\n                                case \"SHA256\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSASHA256;\n                                    break;\n\n                                case \"SHA512\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSASHA512;\n                                    break;\n\n                                default:\n                                    throw new NotSupportedException(\"Hash algorithm is not supported: \" + hashAlgorithm);\n                            }\n\n                            if (pemKskPrivateKey is null)\n                                kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, request.GetQueryOrForm(\"kskKeySize\", int.Parse));\n                            else\n                                kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, pemKskPrivateKey);\n\n                            if (pemZskPrivateKey is null)\n                                zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, request.GetQueryOrForm(\"zskKeySize\", int.Parse));\n                            else\n                                zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, pemZskPrivateKey);\n                        }\n                        break;\n\n                    case \"ECDSA\":\n                        {\n                            string curve = request.GetQueryOrForm(\"curve\");\n\n                            DnssecAlgorithm dnssecAlgorithm;\n\n                            switch (curve.ToUpper())\n                            {\n                                case \"P256\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ECDSAP256SHA256;\n                                    break;\n\n                                case \"P384\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ECDSAP384SHA384;\n                                    break;\n\n                                default:\n                                    throw new NotSupportedException(\"ECDSA curve is not supported: \" + curve);\n                            }\n\n                            if (pemKskPrivateKey is null)\n                                kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey);\n                            else\n                                kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, pemKskPrivateKey);\n\n                            if (pemZskPrivateKey is null)\n                                zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey);\n                            else\n                                zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, pemZskPrivateKey);\n                        }\n                        break;\n\n                    case \"EDDSA\":\n                        {\n                            string curve = request.GetQueryOrForm(\"curve\");\n\n                            DnssecAlgorithm dnssecAlgorithm;\n\n                            switch (curve.ToUpper())\n                            {\n                                case \"ED25519\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ED25519;\n                                    break;\n\n                                case \"ED448\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ED448;\n                                    break;\n\n                                default:\n                                    throw new NotSupportedException(\"EdDSA curve is not supported: \" + curve);\n                            }\n\n                            if (pemKskPrivateKey is null)\n                                kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey);\n                            else\n                                kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, pemKskPrivateKey);\n\n                            if (pemZskPrivateKey is null)\n                                zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey);\n                            else\n                                zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, pemZskPrivateKey);\n                        }\n                        break;\n\n                    default:\n                        throw new NotSupportedException(\"Algorithm is not supported: \" + algorithm);\n                }\n\n                zskPrivateKey.RolloverDays = zskRolloverDays;\n\n                _dnsWebService._dnsServer.AuthZoneManager.SignPrimaryZone(zoneName, kskPrivateKey, zskPrivateKey, dnsKeyTtl, useNSEC3, iterations, saltLength);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary zone was signed successfully: \" + zoneName);\n            }\n\n            public void UnsignPrimaryZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.AuthZoneManager.UnsignPrimaryZone(zoneName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary zone was unsigned successfully: \" + zoneName);\n            }\n\n            public void GetPrimaryZoneDsInfo(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (zoneInfo.Type != AuthZoneType.Primary)\n                    throw new DnsWebServiceException(\"The zone must be a primary zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                if (zoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned)\n                    throw new DnsWebServiceException(\"The zone must be signed with DNSSEC.\");\n\n                IReadOnlyList<DnsResourceRecord> dnsKeyRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.DNSKEY);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"name\", zoneInfo.Name);\n                jsonWriter.WriteString(\"type\", zoneInfo.Type.ToString());\n                jsonWriter.WriteBoolean(\"internal\", zoneInfo.Internal);\n                jsonWriter.WriteBoolean(\"disabled\", zoneInfo.Disabled);\n                jsonWriter.WriteString(\"dnssecStatus\", zoneInfo.ApexZone.DnssecStatus.ToString());\n\n                jsonWriter.WritePropertyName(\"dsRecords\");\n                jsonWriter.WriteStartArray();\n\n                foreach (DnsResourceRecord record in dnsKeyRecords)\n                {\n                    if (record.RDATA is DnsDNSKEYRecordData rdata && rdata.Flags.HasFlag(DnsDnsKeyFlag.SecureEntryPoint))\n                    {\n                        jsonWriter.WriteStartObject();\n\n                        jsonWriter.WriteNumber(\"keyTag\", rdata.ComputedKeyTag);\n\n                        IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys;\n                        if (dnssecPrivateKeys is not null)\n                        {\n                            foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)\n                            {\n                                if ((dnssecPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (dnssecPrivateKey.KeyTag == rdata.ComputedKeyTag))\n                                {\n                                    jsonWriter.WriteString(\"dnsKeyState\", dnssecPrivateKey.State.ToString());\n\n                                    if (dnssecPrivateKey.State == DnssecPrivateKeyState.Published)\n                                        jsonWriter.WriteString(\"dnsKeyStateReadyBy\", dnssecPrivateKey.StateTransitionByWithDelays);\n\n                                    jsonWriter.WriteBoolean(\"isRetiring\", dnssecPrivateKey.IsRetiring);\n                                    break;\n                                }\n                            }\n                        }\n\n                        jsonWriter.WriteString(\"algorithm\", rdata.Algorithm.ToString());\n                        jsonWriter.WriteNumber(\"algorithmNumber\", (byte)rdata.Algorithm);\n                        jsonWriter.WriteString(\"publicKey\", rdata.PublicKey.ToString());\n\n                        jsonWriter.WritePropertyName(\"digests\");\n                        jsonWriter.WriteStartArray();\n\n                        {\n                            jsonWriter.WriteStartObject();\n\n                            jsonWriter.WriteString(\"digestType\", \"SHA256\");\n                            jsonWriter.WriteString(\"digestTypeNumber\", \"2\");\n                            jsonWriter.WriteString(\"digest\", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA256).Digest));\n\n                            jsonWriter.WriteEndObject();\n                        }\n\n                        {\n                            jsonWriter.WriteStartObject();\n\n                            jsonWriter.WriteString(\"digestType\", \"SHA384\");\n                            jsonWriter.WriteString(\"digestTypeNumber\", \"4\");\n                            jsonWriter.WriteString(\"digest\", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA384).Digest));\n\n                            jsonWriter.WriteEndObject();\n                        }\n\n                        jsonWriter.WriteEndArray();\n\n                        jsonWriter.WriteEndObject();\n                    }\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void GetPrimaryZoneDnssecProperties(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (zoneInfo.Type != AuthZoneType.Primary)\n                    throw new DnsWebServiceException(\"The zone must be a primary zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"name\", zoneInfo.Name);\n                jsonWriter.WriteString(\"type\", zoneInfo.Type.ToString());\n                jsonWriter.WriteBoolean(\"internal\", zoneInfo.Internal);\n                jsonWriter.WriteBoolean(\"disabled\", zoneInfo.Disabled);\n                jsonWriter.WriteString(\"dnssecStatus\", zoneInfo.ApexZone.DnssecStatus.ToString());\n\n                if (zoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3)\n                {\n                    IReadOnlyList<DnsResourceRecord> nsec3ParamRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.NSEC3PARAM);\n                    DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData;\n\n                    jsonWriter.WriteNumber(\"nsec3Iterations\", nsec3Param.Iterations);\n                    jsonWriter.WriteNumber(\"nsec3SaltLength\", nsec3Param.Salt.Length);\n                }\n\n                jsonWriter.WriteNumber(\"dnsKeyTtl\", zoneInfo.DnsKeyTtl);\n\n                jsonWriter.WritePropertyName(\"dnssecPrivateKeys\");\n                jsonWriter.WriteStartArray();\n\n                IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys;\n                if (dnssecPrivateKeys is not null)\n                {\n                    List<DnssecPrivateKey> sortedDnssecPrivateKey = new List<DnssecPrivateKey>(dnssecPrivateKeys);\n\n                    sortedDnssecPrivateKey.Sort(delegate (DnssecPrivateKey key1, DnssecPrivateKey key2)\n                    {\n                        int value = key1.KeyType.CompareTo(key2.KeyType);\n                        if (value == 0)\n                            value = key1.StateChangedOn.CompareTo(key2.StateChangedOn);\n\n                        return value;\n                    });\n\n                    foreach (DnssecPrivateKey dnssecPrivateKey in sortedDnssecPrivateKey)\n                        WriteDnssecPrivateKeyAsJson(dnssecPrivateKey, jsonWriter);\n                }\n\n                jsonWriter.WriteEndArray();\n            }\n\n            public void ConvertPrimaryZoneToNSEC(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.AuthZoneManager.ConvertPrimaryZoneToNSEC(zoneName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary zone was converted to NSEC successfully: \" + zoneName);\n            }\n\n            public void ConvertPrimaryZoneToNSEC3(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                ushort iterations = request.GetQueryOrForm<ushort>(\"iterations\", ushort.Parse, 0);\n                byte saltLength = request.GetQueryOrForm<byte>(\"saltLength\", byte.Parse, 0);\n\n                _dnsWebService._dnsServer.AuthZoneManager.ConvertPrimaryZoneToNSEC3(zoneName, iterations, saltLength);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary zone was converted to NSEC3 successfully: \" + zoneName);\n            }\n\n            public void UpdatePrimaryZoneNSEC3Parameters(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                ushort iterations = request.GetQueryOrForm<ushort>(\"iterations\", ushort.Parse, 0);\n                byte saltLength = request.GetQueryOrForm<byte>(\"saltLength\", byte.Parse, 0);\n\n                _dnsWebService._dnsServer.AuthZoneManager.UpdatePrimaryZoneNSEC3Parameters(zoneName, iterations, saltLength);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary zone NSEC3 parameters were updated successfully: \" + zoneName);\n            }\n\n            public void UpdatePrimaryZoneDnssecDnsKeyTtl(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                uint dnsKeyTtl = request.GetQueryOrForm(\"ttl\", ZoneFile.ParseTtl);\n\n                _dnsWebService._dnsServer.AuthZoneManager.UpdatePrimaryZoneDnsKeyTtl(zoneName, dnsKeyTtl);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary zone DNSKEY TTL was updated successfully: \" + zoneName);\n            }\n\n            public void AddPrimaryZoneDnssecPrivateKey(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                DnssecPrivateKeyType keyType = request.GetQueryOrFormEnum<DnssecPrivateKeyType>(\"keyType\");\n                ushort rolloverDays = request.GetQueryOrForm(\"rolloverDays\", ushort.Parse, (ushort)(keyType == DnssecPrivateKeyType.ZoneSigningKey ? 30 : 0));\n                string algorithm = request.GetQueryOrForm(\"algorithm\");\n                string pemPrivateKey = request.GetQueryOrForm(\"pemPrivateKey\", null);\n\n                DnssecPrivateKey privateKey;\n\n                switch (algorithm.ToUpper())\n                {\n                    case \"RSA\":\n                        {\n                            string hashAlgorithm = request.GetQueryOrForm(\"hashAlgorithm\");\n\n                            DnssecAlgorithm dnssecAlgorithm;\n\n                            switch (hashAlgorithm.ToUpper())\n                            {\n                                case \"MD5\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSAMD5;\n                                    break;\n\n                                case \"SHA1\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSASHA1;\n                                    break;\n\n                                case \"SHA256\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSASHA256;\n                                    break;\n\n                                case \"SHA512\":\n                                    dnssecAlgorithm = DnssecAlgorithm.RSASHA512;\n                                    break;\n\n                                default:\n                                    throw new NotSupportedException(\"Hash algorithm is not supported: \" + hashAlgorithm);\n                            }\n\n                            if (pemPrivateKey is null)\n                            {\n                                int keySize = request.GetQueryOrForm(\"keySize\", int.Parse);\n\n                                privateKey = _dnsWebService._dnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecPrivateKey(zoneName, keyType, dnssecAlgorithm, rolloverDays, keySize);\n                            }\n                            else\n                            {\n                                privateKey = DnssecPrivateKey.Create(dnssecAlgorithm, keyType, pemPrivateKey);\n                                privateKey.RolloverDays = rolloverDays;\n\n                                _dnsWebService._dnsServer.AuthZoneManager.AddPrimaryZoneDnssecPrivateKey(zoneName, privateKey);\n                            }\n                        }\n                        break;\n\n                    case \"ECDSA\":\n                        {\n                            string curve = request.GetQueryOrForm(\"curve\");\n\n                            DnssecAlgorithm dnssecAlgorithm;\n\n                            switch (curve.ToUpper())\n                            {\n                                case \"P256\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ECDSAP256SHA256;\n                                    break;\n\n                                case \"P384\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ECDSAP384SHA384;\n                                    break;\n\n                                default:\n                                    throw new NotSupportedException(\"ECDSA curve is not supported: \" + curve);\n                            }\n\n                            if (pemPrivateKey is null)\n                            {\n                                privateKey = _dnsWebService._dnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecPrivateKey(zoneName, keyType, dnssecAlgorithm, rolloverDays);\n                            }\n                            else\n                            {\n                                privateKey = DnssecPrivateKey.Create(dnssecAlgorithm, keyType, pemPrivateKey);\n                                privateKey.RolloverDays = rolloverDays;\n\n                                _dnsWebService._dnsServer.AuthZoneManager.AddPrimaryZoneDnssecPrivateKey(zoneName, privateKey);\n                            }\n                        }\n                        break;\n\n                    case \"EDDSA\":\n                        {\n                            string curve = request.GetQueryOrForm(\"curve\");\n\n                            DnssecAlgorithm dnssecAlgorithm;\n\n                            switch (curve.ToUpper())\n                            {\n                                case \"ED25519\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ED25519;\n                                    break;\n\n                                case \"ED448\":\n                                    dnssecAlgorithm = DnssecAlgorithm.ED448;\n                                    break;\n\n                                default:\n                                    throw new NotSupportedException(\"EdDSA curve is not supported: \" + curve);\n                            }\n\n                            if (pemPrivateKey is null)\n                            {\n                                privateKey = _dnsWebService._dnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecPrivateKey(zoneName, keyType, dnssecAlgorithm, rolloverDays);\n                            }\n                            else\n                            {\n                                privateKey = DnssecPrivateKey.Create(dnssecAlgorithm, keyType, pemPrivateKey);\n                                privateKey.RolloverDays = rolloverDays;\n\n                                _dnsWebService._dnsServer.AuthZoneManager.AddPrimaryZoneDnssecPrivateKey(zoneName, privateKey);\n                            }\n                        }\n                        break;\n\n                    default:\n                        throw new NotSupportedException(\"Algorithm is not supported: \" + algorithm);\n                }\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"addedDnssecPrivateKey\");\n                WriteDnssecPrivateKeyAsJson(privateKey, jsonWriter);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNSSEC private key was generated and added to the primary zone successfully: \" + zoneName);\n            }\n\n            public void UpdatePrimaryZoneDnssecPrivateKey(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                ushort keyTag = request.GetQueryOrForm(\"keyTag\", ushort.Parse);\n                ushort rolloverDays = request.GetQueryOrForm(\"rolloverDays\", ushort.Parse);\n\n                DnssecPrivateKey privateKey = _dnsWebService._dnsServer.AuthZoneManager.UpdatePrimaryZoneDnssecPrivateKey(zoneName, keyTag, rolloverDays);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"updatedDnssecPrivateKey\");\n                WriteDnssecPrivateKeyAsJson(privateKey, jsonWriter);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Primary zone DNSSEC private key config was updated successfully: \" + zoneName);\n            }\n\n            public void DeletePrimaryZoneDnssecPrivateKey(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                ushort keyTag = request.GetQueryOrForm(\"keyTag\", ushort.Parse);\n\n                _dnsWebService._dnsServer.AuthZoneManager.DeletePrimaryZoneDnssecPrivateKey(zoneName, keyTag);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] DNSSEC private key was deleted from primary zone successfully: \" + zoneName);\n            }\n\n            public void PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                _dnsWebService._dnsServer.AuthZoneManager.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(zoneName);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] All DNSSEC private keys from the primary zone were published successfully: \" + zoneName);\n            }\n\n            public void RolloverPrimaryZoneDnsKey(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                ushort keyTag = request.GetQueryOrForm(\"keyTag\", ushort.Parse);\n\n                _dnsWebService._dnsServer.AuthZoneManager.RolloverPrimaryZoneDnsKey(zoneName, keyTag);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] The DNSKEY (\" + keyTag + \") from the primary zone was rolled over successfully: \" + zoneName);\n            }\n\n            public async Task RetirePrimaryZoneDnsKeyAsync(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrForm(\"zone\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                ushort keyTag = request.GetQueryOrForm(\"keyTag\", ushort.Parse);\n\n                await _dnsWebService._dnsServer.AuthZoneManager.RetirePrimaryZoneDnsKeyAsync(zoneName, keyTag);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] The DNSKEY (\" + keyTag + \") from the primary zone was retired successfully: \" + zoneName);\n            }\n\n            public void DeleteZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrFormAlt(\"zone\", \"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                        if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterPrimaryZone(zoneInfo.Name))\n                            throw new DnsWebServiceException(\"Cannot delete the Cluster Primary zone '\" + zoneInfo.DisplayName + \"'.\");\n\n                        break;\n\n                    case AuthZoneType.Catalog:\n                        if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.Name))\n                            throw new DnsWebServiceException(\"Cannot delete the Cluster Catalog zone '\" + zoneInfo.DisplayName + \"'.\");\n\n                        break;\n                }\n\n                if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteZone(zoneInfo, true))\n                    throw new DnsWebServiceException(\"Failed to delete the zone '\" + zoneInfo.DisplayName + \"': no such zone exists.\");\n\n                _dnsWebService._authManager.RemoveAllPermissions(PermissionSection.Zones, zoneInfo.Name);\n                _dnsWebService._authManager.SaveConfigFile();\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] \" + zoneInfo.TypeName + \" zone was deleted: \" + zoneInfo.DisplayName);\n\n                //delete cache for this zone to allow rebuilding cache data without using the current zone\n                _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name);\n            }\n\n            public void EnableZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrFormAlt(\"zone\", \"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                zoneInfo.Disabled = false;\n                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] \" + zoneInfo.TypeName + \" zone was enabled: \" + zoneInfo.DisplayName);\n\n                //delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zone\n                _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name);\n            }\n\n            public void DisableZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrFormAlt(\"zone\", \"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                        if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterPrimaryZone(zoneInfo.Name))\n                            throw new DnsWebServiceException(\"Cannot disable the Cluster Primary zone '\" + zoneInfo.DisplayName + \"'.\");\n\n                        break;\n\n                    case AuthZoneType.Catalog:\n                        if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.Name))\n                            throw new DnsWebServiceException(\"Cannot disable the Cluster Catalog zone '\" + zoneInfo.DisplayName + \"'.\");\n\n                        break;\n                }\n\n                zoneInfo.Disabled = true;\n                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] \" + zoneInfo.TypeName + \" zone was disabled: \" + zoneInfo.DisplayName);\n\n                //delete cache for this zone to allow rebuilding cache data without using the current zone\n                _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name);\n            }\n\n            public void GetZoneOptions(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrFormAlt(\"zone\", \"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                bool includeAvailableCatalogZoneNames = request.GetQueryOrForm(\"includeAvailableCatalogZoneNames\", bool.Parse, false);\n                bool includeAvailableTsigKeyNames = request.GetQueryOrForm(\"includeAvailableTsigKeyNames\", bool.Parse, false);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WriteString(\"name\", zoneInfo.Name);\n\n                if (DnsClient.TryConvertDomainNameToUnicode(zoneInfo.Name, out string nameIdn))\n                    jsonWriter.WriteString(\"nameIdn\", nameIdn);\n\n                jsonWriter.WriteString(\"type\", zoneInfo.Type.ToString());\n\n                if (zoneInfo.Type == AuthZoneType.Primary)\n                    jsonWriter.WriteBoolean(\"internal\", zoneInfo.Internal);\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                        jsonWriter.WriteString(\"dnssecStatus\", zoneInfo.ApexZone.DnssecStatus.ToString());\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                        if (!zoneInfo.Internal)\n                        {\n                            string[] notifyFailed = zoneInfo.NotifyFailed;\n\n                            jsonWriter.WriteBoolean(\"notifyFailed\", notifyFailed.Length > 0);\n\n                            jsonWriter.WritePropertyName(\"notifyFailedFor\");\n                            jsonWriter.WriteStartArray();\n\n                            foreach (string server in notifyFailed)\n                                jsonWriter.WriteStringValue(server);\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n                }\n\n                jsonWriter.WriteBoolean(\"disabled\", zoneInfo.Disabled);\n\n                //catalog zone\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Forwarder:\n                        jsonWriter.WriteString(\"catalog\", zoneInfo.CatalogZoneName);\n\n                        if (zoneInfo.CatalogZoneName is not null)\n                        {\n                            jsonWriter.WriteBoolean(\"overrideCatalogQueryAccess\", zoneInfo.OverrideCatalogQueryAccess);\n                            jsonWriter.WriteBoolean(\"overrideCatalogZoneTransfer\", zoneInfo.OverrideCatalogZoneTransfer);\n                            jsonWriter.WriteBoolean(\"overrideCatalogNotify\", zoneInfo.OverrideCatalogNotify);\n                        }\n\n                        break;\n\n                    case AuthZoneType.Stub:\n                        jsonWriter.WriteString(\"catalog\", zoneInfo.CatalogZoneName);\n\n                        if (zoneInfo.CatalogZoneName is not null)\n                        {\n                            jsonWriter.WriteBoolean(\"isSecondaryCatalogMember\", zoneInfo.ApexZone.SecondaryCatalogZone is not null);\n                            jsonWriter.WriteBoolean(\"overrideCatalogQueryAccess\", zoneInfo.OverrideCatalogQueryAccess);\n                        }\n                        break;\n\n                    case AuthZoneType.Secondary:\n                        jsonWriter.WriteString(\"catalog\", zoneInfo.CatalogZoneName);\n\n                        if (zoneInfo.CatalogZoneName is not null)\n                        {\n                            jsonWriter.WriteBoolean(\"isSecondaryCatalogMember\", zoneInfo.ApexZone.SecondaryCatalogZone is not null);\n                            jsonWriter.WriteBoolean(\"overrideCatalogQueryAccess\", zoneInfo.OverrideCatalogQueryAccess);\n                            jsonWriter.WriteBoolean(\"overrideCatalogZoneTransfer\", zoneInfo.OverrideCatalogZoneTransfer);\n                            jsonWriter.WriteBoolean(\"overrideCatalogPrimaryNameServers\", zoneInfo.OverrideCatalogPrimaryNameServers);\n                        }\n                        break;\n\n                    case AuthZoneType.SecondaryForwarder:\n                        jsonWriter.WriteString(\"catalog\", zoneInfo.CatalogZoneName);\n\n                        if (zoneInfo.CatalogZoneName is not null)\n                            jsonWriter.WriteBoolean(\"overrideCatalogQueryAccess\", zoneInfo.OverrideCatalogQueryAccess);\n\n                        break;\n                }\n\n                //primary server\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                    case AuthZoneType.Stub:\n                        jsonWriter.WriteStartArray(\"primaryNameServerAddresses\");\n\n                        IReadOnlyList<NameServerAddress> primaryNameServerAddresses = zoneInfo.PrimaryNameServerAddresses;\n                        if (primaryNameServerAddresses is not null)\n                        {\n                            foreach (NameServerAddress primaryNameServerAddress in primaryNameServerAddresses)\n                                jsonWriter.WriteStringValue(primaryNameServerAddress.OriginalAddress);\n                        }\n\n                        jsonWriter.WriteEndArray();\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        if (zoneInfo.PrimaryZoneTransferProtocol == DnsTransportProtocol.Udp)\n                            jsonWriter.WriteString(\"primaryZoneTransferProtocol\", \"Tcp\");\n                        else\n                            jsonWriter.WriteString(\"primaryZoneTransferProtocol\", zoneInfo.PrimaryZoneTransferProtocol.ToString());\n\n                        jsonWriter.WriteString(\"primaryZoneTransferTsigKeyName\", zoneInfo.PrimaryZoneTransferTsigKeyName);\n                        break;\n                }\n\n                if (zoneInfo.Type == AuthZoneType.Secondary)\n                    jsonWriter.WriteBoolean(\"validateZone\", zoneInfo.ValidateZone);\n\n                //query access\n                {\n                    jsonWriter.WriteString(\"queryAccess\", zoneInfo.QueryAccess.ToString());\n                    jsonWriter.WriteStartArray(\"queryAccessNetworkACL\");\n\n                    if (zoneInfo.QueryAccessNetworkACL is not null)\n                    {\n                        foreach (NetworkAccessControl nac in zoneInfo.QueryAccessNetworkACL)\n                            jsonWriter.WriteStringValue(nac.ToString());\n                    }\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                //zone transfer\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                    case AuthZoneType.SecondaryCatalog:\n                        jsonWriter.WriteString(\"zoneTransfer\", zoneInfo.ZoneTransfer.ToString());\n\n                        jsonWriter.WritePropertyName(\"zoneTransferNetworkACL\");\n                        {\n                            jsonWriter.WriteStartArray();\n\n                            if (zoneInfo.ZoneTransferNetworkACL is not null)\n                            {\n                                foreach (NetworkAccessControl nac in zoneInfo.ZoneTransferNetworkACL)\n                                    jsonWriter.WriteStringValue(nac.ToString());\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n\n                        jsonWriter.WritePropertyName(\"zoneTransferTsigKeyNames\");\n                        {\n                            jsonWriter.WriteStartArray();\n\n                            if (zoneInfo.ZoneTransferTsigKeyNames is not null)\n                            {\n                                foreach (string tsigKeyName in zoneInfo.ZoneTransferTsigKeyNames)\n                                    jsonWriter.WriteStringValue(tsigKeyName);\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n\n                        break;\n                }\n\n                //notify\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                        jsonWriter.WriteString(\"notify\", zoneInfo.Notify.ToString());\n\n                        jsonWriter.WritePropertyName(\"notifyNameServers\");\n                        {\n                            jsonWriter.WriteStartArray();\n\n                            if (zoneInfo.NotifyNameServers is not null)\n                            {\n                                foreach (IPAddress nameServer in zoneInfo.NotifyNameServers)\n                                    jsonWriter.WriteStringValue(nameServer.ToString());\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n\n                        if (zoneInfo.Type == AuthZoneType.Catalog)\n                        {\n                            jsonWriter.WriteStartArray(\"notifySecondaryCatalogsNameServers\");\n\n                            if (zoneInfo.NotifySecondaryCatalogNameServers is not null)\n                            {\n                                foreach (IPAddress nameServer in zoneInfo.NotifySecondaryCatalogNameServers)\n                                    jsonWriter.WriteStringValue(nameServer.ToString());\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n                }\n\n                //update\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.Forwarder:\n                        jsonWriter.WriteString(\"update\", zoneInfo.Update.ToString());\n\n                        jsonWriter.WritePropertyName(\"updateNetworkACL\");\n                        {\n                            jsonWriter.WriteStartArray();\n\n                            if (zoneInfo.UpdateNetworkACL is not null)\n                            {\n                                foreach (NetworkAccessControl nac in zoneInfo.UpdateNetworkACL)\n                                    jsonWriter.WriteStringValue(nac.ToString());\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Forwarder:\n                        jsonWriter.WritePropertyName(\"updateSecurityPolicies\");\n                        {\n                            jsonWriter.WriteStartArray();\n\n                            if (zoneInfo.UpdateSecurityPolicies is not null)\n                            {\n                                foreach (KeyValuePair<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicy in zoneInfo.UpdateSecurityPolicies)\n                                {\n                                    foreach (KeyValuePair<string, IReadOnlyList<DnsResourceRecordType>> policy in updateSecurityPolicy.Value)\n                                    {\n                                        jsonWriter.WriteStartObject();\n\n                                        jsonWriter.WriteString(\"tsigKeyName\", updateSecurityPolicy.Key);\n                                        jsonWriter.WriteString(\"domain\", policy.Key);\n\n                                        jsonWriter.WritePropertyName(\"allowedTypes\");\n                                        jsonWriter.WriteStartArray();\n\n                                        foreach (DnsResourceRecordType allowedType in policy.Value)\n                                            jsonWriter.WriteStringValue(allowedType.ToString());\n\n                                        jsonWriter.WriteEndArray();\n\n                                        jsonWriter.WriteEndObject();\n                                    }\n                                }\n                            }\n\n                            jsonWriter.WriteEndArray();\n                        }\n                        break;\n                }\n\n                if (includeAvailableCatalogZoneNames)\n                {\n                    IReadOnlyList<AuthZoneInfo> catalogZoneInfoList = _dnsWebService._dnsServer.AuthZoneManager.GetCatalogZones(delegate (AuthZoneInfo catalogZoneInfo)\n                    {\n                        return !catalogZoneInfo.Disabled && _dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify);\n                    });\n\n                    jsonWriter.WritePropertyName(\"availableCatalogZoneNames\");\n                    jsonWriter.WriteStartArray();\n\n                    foreach (AuthZoneInfo catalogZoneInfo in catalogZoneInfoList)\n                        jsonWriter.WriteStringValue(catalogZoneInfo.Name);\n\n                    jsonWriter.WriteEndArray();\n                }\n\n                if (includeAvailableTsigKeyNames)\n                {\n                    jsonWriter.WritePropertyName(\"availableTsigKeyNames\");\n                    {\n                        jsonWriter.WriteStartArray();\n\n                        if (_dnsWebService._dnsServer.TsigKeys is not null)\n                        {\n                            foreach (KeyValuePair<string, TsigKey> tsigKey in _dnsWebService._dnsServer.TsigKeys)\n                                jsonWriter.WriteStringValue(tsigKey.Key);\n                        }\n\n                        jsonWriter.WriteEndArray();\n                    }\n                }\n            }\n\n            public void SetZoneOptions(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                HttpRequest request = context.Request;\n\n                string zoneName = request.GetQueryOrFormAlt(\"zone\", \"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                if (request.TryGetQueryOrForm(\"disabled\", bool.Parse, out bool disabled))\n                    zoneInfo.Disabled = disabled;\n\n                //catalog zone override options\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Forwarder:\n                        {\n                            if (request.TryGetQueryOrForm(\"overrideCatalogQueryAccess\", bool.Parse, out bool overrideCatalogQueryAccess))\n                                zoneInfo.OverrideCatalogQueryAccess = overrideCatalogQueryAccess;\n\n                            if (request.TryGetQueryOrForm(\"overrideCatalogZoneTransfer\", bool.Parse, out bool overrideCatalogZoneTransfer))\n                                zoneInfo.OverrideCatalogZoneTransfer = overrideCatalogZoneTransfer;\n\n                            if (request.TryGetQueryOrForm(\"overrideCatalogNotify\", bool.Parse, out bool overrideCatalogNotify))\n                                zoneInfo.OverrideCatalogNotify = overrideCatalogNotify;\n                        }\n                        break;\n\n                    case AuthZoneType.Secondary:\n                        {\n                            if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                                break; //cannot set option for Secondary zone that is a member of Secondary Catalog Zone\n\n                            if (request.TryGetQueryOrForm(\"overrideCatalogQueryAccess\", bool.Parse, out bool overrideCatalogQueryAccess))\n                                zoneInfo.OverrideCatalogQueryAccess = overrideCatalogQueryAccess;\n\n                            if (request.TryGetQueryOrForm(\"overrideCatalogZoneTransfer\", bool.Parse, out bool overrideCatalogZoneTransfer))\n                                zoneInfo.OverrideCatalogZoneTransfer = overrideCatalogZoneTransfer;\n                        }\n                        break;\n\n                    case AuthZoneType.Stub:\n                        {\n                            if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                                break; //cannot set option for Stub zone that is a member of Secondary Catalog Zone\n\n                            if (request.TryGetQueryOrForm(\"overrideCatalogQueryAccess\", bool.Parse, out bool overrideCatalogQueryAccess))\n                                zoneInfo.OverrideCatalogQueryAccess = overrideCatalogQueryAccess;\n                        }\n                        break;\n                }\n\n                //primary server\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                        {\n                            if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                                break; //cannot set option for zone that is a member of Secondary Catalog Zone\n\n                            if (request.TryGetQueryOrFormEnum(\"primaryZoneTransferProtocol\", out DnsTransportProtocol primaryZoneTransferProtocol))\n                            {\n                                if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic)\n                                    DnsWebService.ValidateQuicSupport();\n\n                                zoneInfo.PrimaryZoneTransferProtocol = primaryZoneTransferProtocol;\n                            }\n\n                            string primaryNameServerAddresses = request.QueryOrForm(\"primaryNameServerAddresses\");\n                            if (primaryNameServerAddresses is not null)\n                            {\n                                if (primaryNameServerAddresses.Length == 0)\n                                {\n                                    zoneInfo.PrimaryNameServerAddresses = null;\n                                }\n                                else\n                                {\n                                    zoneInfo.PrimaryNameServerAddresses = primaryNameServerAddresses.Split(delegate (string address)\n                                    {\n                                        NameServerAddress nameServer = NameServerAddress.Parse(address);\n\n                                        if (nameServer.Protocol != primaryZoneTransferProtocol)\n                                            nameServer = nameServer.Clone(primaryZoneTransferProtocol);\n\n                                        return nameServer;\n                                    }, ',');\n                                }\n                            }\n\n                            string primaryZoneTransferTsigKeyName = request.QueryOrForm(\"primaryZoneTransferTsigKeyName\");\n                            if (primaryZoneTransferTsigKeyName is not null)\n                            {\n                                if (primaryZoneTransferTsigKeyName.Length == 0)\n                                    zoneInfo.PrimaryZoneTransferTsigKeyName = null;\n                                else\n                                    zoneInfo.PrimaryZoneTransferTsigKeyName = primaryZoneTransferTsigKeyName;\n                            }\n                        }\n                        break;\n\n                    case AuthZoneType.Stub:\n                        {\n                            if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                                break; //cannot set option for Stub zone that is a member of Secondary Catalog Zone\n\n                            string primaryNameServerAddresses = request.QueryOrForm(\"primaryNameServerAddresses\");\n                            if (primaryNameServerAddresses is not null)\n                            {\n                                if (primaryNameServerAddresses.Length == 0)\n                                {\n                                    zoneInfo.PrimaryNameServerAddresses = null;\n                                }\n                                else\n                                {\n                                    zoneInfo.PrimaryNameServerAddresses = primaryNameServerAddresses.Split(delegate (string address)\n                                    {\n                                        NameServerAddress nameServer = NameServerAddress.Parse(address);\n\n                                        if (nameServer.Protocol != DnsTransportProtocol.Udp)\n                                            nameServer = nameServer.Clone(DnsTransportProtocol.Udp);\n\n                                        return nameServer;\n                                    }, ',');\n                                }\n                            }\n                        }\n                        break;\n                }\n\n                if (zoneInfo.Type == AuthZoneType.Secondary)\n                {\n                    if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                    {\n                        //cannot set option for zone that is a member of Secondary Catalog Zone\n                    }\n                    else if (request.TryGetQueryOrForm(\"validateZone\", bool.Parse, out bool validateZone))\n                    {\n                        zoneInfo.ValidateZone = validateZone;\n                    }\n                }\n\n                //query access\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Stub:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.Catalog:\n                        if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                            break; //cannot set option for zone that is a member of Secondary Catalog Zone\n\n                        string queryAccessNetworkACL = request.QueryOrForm(\"queryAccessNetworkACL\");\n                        if (queryAccessNetworkACL is not null)\n                        {\n                            if ((queryAccessNetworkACL.Length == 0) || queryAccessNetworkACL.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                                zoneInfo.QueryAccessNetworkACL = null;\n                            else\n                                zoneInfo.QueryAccessNetworkACL = queryAccessNetworkACL.Split(NetworkAccessControl.Parse, ',');\n                        }\n\n                        if (request.TryGetQueryOrFormEnum(\"queryAccess\", out AuthZoneQueryAccess queryAccess))\n                            zoneInfo.QueryAccess = queryAccess;\n\n                        break;\n                }\n\n                //zone transfer\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                        if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                            break; //cannot set option for zone that is a member of Secondary Catalog Zone\n\n                        string strZoneTransferNetworkACL = request.QueryOrForm(\"zoneTransferNetworkACL\");\n                        if (strZoneTransferNetworkACL is not null)\n                        {\n                            if ((strZoneTransferNetworkACL.Length == 0) || strZoneTransferNetworkACL.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                                zoneInfo.ZoneTransferNetworkACL = null;\n                            else\n                                zoneInfo.ZoneTransferNetworkACL = strZoneTransferNetworkACL.Split(NetworkAccessControl.Parse, ',');\n                        }\n\n                        if (request.TryGetQueryOrFormEnum(\"zoneTransfer\", out AuthZoneTransfer zoneTransfer))\n                            zoneInfo.ZoneTransfer = zoneTransfer;\n\n                        string strZoneTransferTsigKeyNames = request.QueryOrForm(\"zoneTransferTsigKeyNames\");\n                        if (strZoneTransferTsigKeyNames is not null)\n                        {\n                            if ((strZoneTransferTsigKeyNames.Length == 0) || strZoneTransferTsigKeyNames.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                zoneInfo.ZoneTransferTsigKeyNames = null;\n                            }\n                            else\n                            {\n                                string[] strZoneTransferTsigKeyNamesParts = strZoneTransferTsigKeyNames.Split(_commaSeparator, StringSplitOptions.RemoveEmptyEntries);\n                                HashSet<string> zoneTransferTsigKeyNames = new HashSet<string>(strZoneTransferTsigKeyNamesParts.Length);\n\n                                for (int i = 0; i < strZoneTransferTsigKeyNamesParts.Length; i++)\n                                    zoneTransferTsigKeyNames.Add(strZoneTransferTsigKeyNamesParts[i].Trim('.').ToLowerInvariant());\n\n                                zoneInfo.ZoneTransferTsigKeyNames = zoneTransferTsigKeyNames;\n                            }\n                        }\n\n                        break;\n                }\n\n                //notify\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Forwarder:\n                    case AuthZoneType.Catalog:\n                        if (request.TryGetQueryOrFormEnum(\"notify\", out AuthZoneNotify notify))\n                            zoneInfo.Notify = notify;\n\n                        string strNotifyNameServers = request.QueryOrForm(\"notifyNameServers\");\n                        if (strNotifyNameServers is not null)\n                        {\n                            if ((strNotifyNameServers.Length == 0) || strNotifyNameServers.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                                zoneInfo.NotifyNameServers = null;\n                            else\n                                zoneInfo.NotifyNameServers = strNotifyNameServers.Split(IPAddress.Parse, ',');\n                        }\n\n                        if (zoneInfo.Type == AuthZoneType.Catalog)\n                        {\n                            string strNotifySecondaryCatalogNameServers = request.QueryOrForm(\"notifySecondaryCatalogsNameServers\");\n                            if (strNotifySecondaryCatalogNameServers is not null)\n                            {\n                                if ((strNotifySecondaryCatalogNameServers.Length == 0) || strNotifySecondaryCatalogNameServers.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                                    zoneInfo.NotifySecondaryCatalogNameServers = null;\n                                else\n                                    zoneInfo.NotifySecondaryCatalogNameServers = strNotifySecondaryCatalogNameServers.Split(IPAddress.Parse, ',');\n                            }\n                        }\n\n                        break;\n                }\n\n                //update\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.Forwarder:\n                        if (request.TryGetQueryOrFormEnum(\"update\", out AuthZoneUpdate update))\n                            zoneInfo.Update = update;\n\n                        string strUpdateNetworkACL = request.QueryOrForm(\"updateNetworkACL\");\n                        if (strUpdateNetworkACL is not null)\n                        {\n                            if ((strUpdateNetworkACL.Length == 0) || strUpdateNetworkACL.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                                zoneInfo.UpdateNetworkACL = null;\n                            else\n                                zoneInfo.UpdateNetworkACL = strUpdateNetworkACL.Split(NetworkAccessControl.Parse, ',');\n                        }\n                        break;\n                }\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Forwarder:\n                        string strUpdateSecurityPolicies = request.QueryOrForm(\"updateSecurityPolicies\");\n                        if (strUpdateSecurityPolicies is not null)\n                        {\n                            if ((strUpdateSecurityPolicies.Length == 0) || strUpdateSecurityPolicies.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                zoneInfo.UpdateSecurityPolicies = null;\n                            }\n                            else\n                            {\n                                string[] strUpdateSecurityPoliciesParts = strUpdateSecurityPolicies.Split(_pipeSeparator, StringSplitOptions.RemoveEmptyEntries);\n                                Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>> updateSecurityPolicies = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>>>(strUpdateSecurityPoliciesParts.Length);\n\n                                for (int i = 0; i < strUpdateSecurityPoliciesParts.Length; i += 3)\n                                {\n                                    string tsigKeyName = strUpdateSecurityPoliciesParts[i].Trim('.').ToLowerInvariant();\n                                    string domain = strUpdateSecurityPoliciesParts[i + 1].Trim('.').ToLowerInvariant();\n                                    string strTypes = strUpdateSecurityPoliciesParts[i + 2];\n\n                                    if (!domain.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) && !domain.EndsWith(\".\" + zoneInfo.Name, StringComparison.OrdinalIgnoreCase))\n                                        throw new DnsWebServiceException(\"Cannot set Dynamic Updates security policies: the domain '\" + domain + \"' must be part of the current zone.\");\n\n                                    if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary<string, IReadOnlyList<DnsResourceRecordType>> policyMap))\n                                    {\n                                        policyMap = new Dictionary<string, IReadOnlyList<DnsResourceRecordType>>();\n                                        updateSecurityPolicies.Add(tsigKeyName, policyMap);\n                                    }\n\n                                    if (!policyMap.TryGetValue(domain, out IReadOnlyList<DnsResourceRecordType> types))\n                                    {\n                                        types = new List<DnsResourceRecordType>();\n                                        (policyMap as Dictionary<string, IReadOnlyList<DnsResourceRecordType>>).Add(domain, types);\n                                    }\n\n                                    foreach (string strType in strTypes.Split(_commaSpaceSeparator, StringSplitOptions.RemoveEmptyEntries))\n                                        (types as List<DnsResourceRecordType>).Add(Enum.Parse<DnsResourceRecordType>(strType, true));\n                                }\n\n                                zoneInfo.UpdateSecurityPolicies = updateSecurityPolicies;\n                            }\n                        }\n                        break;\n                }\n\n                //catalog zone; done last to allow using updated properties when there is change of ownership\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Primary:\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.Stub:\n                    case AuthZoneType.Forwarder:\n                        if (zoneInfo.ApexZone.SecondaryCatalogZone is not null)\n                            break; //cannot set option for Stub zone that is a member of Secondary Catalog Zone\n\n                        string catalogZoneName = request.QueryOrForm(\"catalog\");\n                        if (catalogZoneName is not null)\n                        {\n                            string oldCatalogZoneName = zoneInfo.CatalogZoneName;\n\n                            if (catalogZoneName.Length == 0)\n                            {\n                                if (!string.IsNullOrEmpty(oldCatalogZoneName))\n                                    _dnsWebService._dnsServer.AuthZoneManager.RemoveCatalogMemberZone(zoneInfo);\n                            }\n                            else\n                            {\n                                if (string.IsNullOrEmpty(oldCatalogZoneName))\n                                {\n                                    //check catalog permissions\n                                    AuthZoneInfo catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName);\n                                    if (catalogZoneInfo is null)\n                                        throw new DnsWebServiceException(\"No such Catalog zone was found: \" + catalogZoneName);\n\n                                    if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                                        throw new DnsWebServiceException(\"Access was denied to use Catalog zone: \" + catalogZoneInfo.Name);\n\n                                    _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo);\n\n                                    if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneInfo.Name))\n                                        _dnsWebService._clusterManager.UpdateClusterRecordsFor(zoneInfo);\n                                }\n                                else if (!catalogZoneName.Equals(oldCatalogZoneName, StringComparison.OrdinalIgnoreCase))\n                                {\n                                    //check catalog permissions\n                                    AuthZoneInfo catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName);\n                                    if (catalogZoneInfo is null)\n                                        throw new DnsWebServiceException(\"No such Catalog zone was found: \" + catalogZoneName);\n\n                                    if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                                        throw new DnsWebServiceException(\"Access was denied to use Catalog zone: \" + catalogZoneInfo.Name);\n\n                                    _dnsWebService._dnsServer.AuthZoneManager.ChangeCatalogMemberZoneOwnership(zoneInfo, catalogZoneInfo.Name);\n\n                                    if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneInfo.Name))\n                                        _dnsWebService._clusterManager.UpdateClusterRecordsFor(zoneInfo);\n                                }\n                            }\n                        }\n\n                        if (zoneInfo.ApexZone.CatalogZone is not null)\n                            _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.ApexZone.CatalogZoneName);\n\n                        break;\n                }\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] \" + zoneInfo.TypeName + \" zone options were updated successfully: \" + zoneInfo.DisplayName);\n\n                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);\n            }\n\n            public void ResyncZone(HttpContext context)\n            {\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string zoneName = context.Request.GetQueryOrFormAlt(\"zone\", \"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(zoneName))\n                    zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + zoneName);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                switch (zoneInfo.Type)\n                {\n                    case AuthZoneType.Secondary:\n                    case AuthZoneType.SecondaryForwarder:\n                    case AuthZoneType.SecondaryCatalog:\n                    case AuthZoneType.Stub:\n                        zoneInfo.TriggerResync();\n                        break;\n\n                    default:\n                        throw new DnsWebServiceException(\"Only Secondary, Secondary Forwarder, Secondary Catalog, and Stub zones support resync.\");\n                }\n            }\n\n            public void AddRecord(HttpContext context)\n            {\n                HttpRequest request = context.Request;\n\n                string domain = request.GetQueryOrForm(\"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                string zoneName = request.QueryOrForm(\"zone\");\n                if (zoneName is not null)\n                {\n                    zoneName = zoneName.Trim('.');\n\n                    if (DnsClient.IsDomainNameUnicode(zoneName))\n                        zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n                }\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + domain);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                DnsResourceRecordType type = request.GetQueryOrFormEnum<DnsResourceRecordType>(\"type\");\n\n                uint defaultTtl;\n\n                switch (type)\n                {\n                    case DnsResourceRecordType.NS:\n                        defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl;\n                        break;\n\n                    default:\n                        defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl;\n                        break;\n                }\n\n                uint ttl = request.GetQueryOrForm(\"ttl\", ZoneFile.ParseTtl, defaultTtl);\n                bool overwrite = request.GetQueryOrForm(\"overwrite\", bool.Parse, false);\n                string comments = request.QueryOrForm(\"comments\");\n                uint expiryTtl = request.GetQueryOrForm(\"expiryTtl\", ZoneFile.ParseTtl, 0u);\n\n                DnsResourceRecord newRecord;\n\n                switch (type)\n                {\n                    case DnsResourceRecordType.A:\n                    case DnsResourceRecordType.AAAA:\n                        {\n                            string strIPAddress = request.GetQueryOrFormAlt(\"ipAddress\", \"value\");\n                            IPAddress ipAddress;\n\n                            if (strIPAddress.Equals(\"request-ip-address\"))\n                                ipAddress = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader).Address;\n                            else\n                                ipAddress = IPAddress.Parse(strIPAddress);\n\n                            bool ptr = request.GetQueryOrForm(\"ptr\", bool.Parse, false);\n                            if (ptr)\n                            {\n                                string ptrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128);\n\n                                AuthZoneInfo reverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain);\n                                if (reverseZoneInfo is null)\n                                {\n                                    bool createPtrZone = request.GetQueryOrForm(\"createPtrZone\", bool.Parse, false);\n                                    if (!createPtrZone)\n                                        throw new DnsWebServiceException(\"No reverse zone available to add PTR record.\");\n\n                                    string ptrZone = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 24 : 64);\n\n                                    reverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(ptrZone);\n                                    if (reverseZoneInfo == null)\n                                        throw new DnsWebServiceException(\"Failed to create reverse zone to add PTR record: \" + ptrZone);\n\n                                    //set permissions\n                                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                                    _dnsWebService._authManager.SaveConfigFile();\n                                }\n\n                                if (reverseZoneInfo.Internal)\n                                    throw new DnsWebServiceException(\"Reverse zone '\" + reverseZoneInfo.DisplayName + \"' is an internal zone.\");\n\n                                if ((reverseZoneInfo.Type != AuthZoneType.Primary) && (reverseZoneInfo.Type != AuthZoneType.Forwarder))\n                                    throw new DnsWebServiceException(\"Reverse zone '\" + reverseZoneInfo.DisplayName + \"' is not a primary or forwarder zone.\");\n\n                                DnsResourceRecord ptrRecord = new DnsResourceRecord(ptrDomain, DnsResourceRecordType.PTR, DnsClass.IN, ttl, new DnsPTRRecordData(domain));\n                                ptrRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n                                ptrRecord.GetAuthGenericRecordInfo().ExpiryTtl = expiryTtl;\n\n                                _dnsWebService._dnsServer.AuthZoneManager.SetRecord(reverseZoneInfo.Name, ptrRecord);\n                                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name);\n                            }\n\n                            if (type == DnsResourceRecordType.A)\n                                newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsARecordData(ipAddress));\n                            else\n                                newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsAAAARecordData(ipAddress));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NS:\n                        {\n                            if ((zoneInfo.Type == AuthZoneType.Primary) && zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName))\n                                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.\");\n\n                            string nameServer = request.GetQueryOrFormAlt(\"nameServer\", \"value\").Trim('.');\n                            string glueAddresses = request.GetQueryOrForm(\"glue\", null);\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsNSRecordData(nameServer));\n\n                            if (!string.IsNullOrEmpty(glueAddresses))\n                            {\n                                if (zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && (nameServer.Equals(domain, StringComparison.OrdinalIgnoreCase) || nameServer.EndsWith(\".\" + domain, StringComparison.OrdinalIgnoreCase)))\n                                    throw new DnsWebServiceException(\"The zone's own NS records cannot have glue addresses. Please add separate A/AAAA records in the zone instead.\");\n\n                                newRecord.SetGlueRecords(glueAddresses);\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CNAME:\n                        {\n                            if (!overwrite)\n                            {\n                                IReadOnlyList<DnsResourceRecord> existingRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneInfo.Name, domain, type);\n                                if (existingRecords.Count > 0)\n                                    throw new DnsWebServiceException(\"Record already exists. Use overwrite option if you wish to overwrite existing record.\");\n                            }\n\n                            string cname = request.GetQueryOrFormAlt(\"cname\", \"value\").Trim('.');\n\n                            if (cname.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                                throw new DnsWebServiceException(\"CNAME domain name cannot be same as that of the record name.\");\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname));\n\n                            overwrite = true; //force SetRecord\n                        }\n                        break;\n\n                    case DnsResourceRecordType.PTR:\n                        {\n                            string ptrName = request.GetQueryOrFormAlt(\"ptrName\", \"value\").Trim('.');\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsPTRRecordData(ptrName));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.MX:\n                        {\n                            ushort preference = request.GetQueryOrForm(\"preference\", ushort.Parse);\n                            string exchange = request.GetQueryOrFormAlt(\"exchange\", \"value\").Trim('.');\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsMXRecordData(preference, exchange));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TXT:\n                        {\n                            string text = request.GetQueryOrFormAlt(\"text\", \"value\");\n                            bool splitText = request.GetQueryOrForm(\"splitText\", bool.Parse, false);\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, splitText ? new DnsTXTRecordData(DecodeCharacterStrings(text)) : new DnsTXTRecordData(text));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.RP:\n                        {\n                            string mailbox = request.GetQueryOrForm(\"mailbox\", \"\").Trim('.');\n                            string txtDomain = request.GetQueryOrForm(\"txtDomain\", \"\").Trim('.');\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsRPRecordData(mailbox, txtDomain));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SRV:\n                        {\n                            ushort priority = request.GetQueryOrForm(\"priority\", ushort.Parse);\n                            ushort weight = request.GetQueryOrForm(\"weight\", ushort.Parse);\n                            ushort port = request.GetQueryOrForm(\"port\", ushort.Parse);\n                            string target = request.GetQueryOrFormAlt(\"target\", \"value\").Trim('.');\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSRVRecordData(priority, weight, port, target));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NAPTR:\n                        {\n                            ushort order = request.GetQueryOrForm(\"naptrOrder\", ushort.Parse);\n                            ushort preference = request.GetQueryOrForm(\"naptrPreference\", ushort.Parse);\n                            string flags = request.GetQueryOrForm(\"naptrFlags\", \"\");\n                            string services = request.GetQueryOrForm(\"naptrServices\", \"\");\n                            string regexp = request.GetQueryOrForm(\"naptrRegexp\", \"\");\n                            string replacement = request.GetQueryOrForm(\"naptrReplacement\", \"\").Trim('.');\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsNAPTRRecordData(order, preference, flags, services, regexp, replacement));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DNAME:\n                        {\n                            if (!overwrite)\n                            {\n                                IReadOnlyList<DnsResourceRecord> existingRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneInfo.Name, domain, type);\n                                if (existingRecords.Count > 0)\n                                    throw new DnsWebServiceException(\"Record already exists. Use overwrite option if you wish to overwrite existing record.\");\n                            }\n\n                            string dname = request.GetQueryOrFormAlt(\"dname\", \"value\").Trim('.');\n\n                            if (dname.EndsWith(\".\" + domain, StringComparison.OrdinalIgnoreCase))\n                                throw new DnsWebServiceException(\"DNAME domain name cannot be a sub domain of the record name.\");\n\n                            if (dname.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                                throw new DnsWebServiceException(\"DNAME domain name cannot be same as that of the record name.\");\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname));\n\n                            overwrite = true; //force SetRecord\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DS:\n                        {\n                            ushort keyTag = request.GetQueryOrForm(\"keyTag\", ushort.Parse);\n                            DnssecAlgorithm algorithm = Enum.Parse<DnssecAlgorithm>(request.GetQueryOrForm(\"algorithm\").Replace('-', '_'), true);\n                            DnssecDigestType digestType = Enum.Parse<DnssecDigestType>(request.GetQueryOrForm(\"digestType\").Replace('-', '_'), true);\n                            byte[] digest = request.GetQueryOrFormAlt(\"digest\", \"value\", Convert.FromHexString);\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDSRecordData(keyTag, algorithm, digestType, digest));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SSHFP:\n                        {\n                            DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum<DnsSSHFPAlgorithm>(\"sshfpAlgorithm\");\n                            DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum<DnsSSHFPFingerprintType>(\"sshfpFingerprintType\");\n                            byte[] sshfpFingerprint = request.GetQueryOrForm(\"sshfpFingerprint\", Convert.FromHexString);\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TLSA:\n                        {\n                            DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse<DnsTLSACertificateUsage>(request.GetQueryOrForm(\"tlsaCertificateUsage\").Replace('-', '_'), true);\n                            DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum<DnsTLSASelector>(\"tlsaSelector\");\n                            DnsTLSAMatchingType tlsaMatchingType = Enum.Parse<DnsTLSAMatchingType>(request.GetQueryOrForm(\"tlsaMatchingType\").Replace('-', '_'), true);\n                            string tlsaCertificateAssociationData = request.GetQueryOrForm(\"tlsaCertificateAssociationData\");\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SVCB:\n                    case DnsResourceRecordType.HTTPS:\n                        {\n                            ushort svcPriority = request.GetQueryOrForm(\"svcPriority\", ushort.Parse);\n                            string targetName = request.GetQueryOrForm(\"svcTargetName\").Trim('.');\n                            string strSvcParams = request.GetQueryOrForm(\"svcParams\");\n                            bool autoIpv4Hint = request.GetQueryOrForm(\"autoIpv4Hint\", bool.Parse, false);\n                            bool autoIpv6Hint = request.GetQueryOrForm(\"autoIpv6Hint\", bool.Parse, false);\n\n                            Dictionary<DnsSvcParamKey, DnsSvcParamValue> svcParams;\n\n                            if (strSvcParams.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                svcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(0);\n                            }\n                            else\n                            {\n                                string[] strSvcParamsParts = strSvcParams.Split('|');\n                                svcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(strSvcParamsParts.Length / 2);\n\n                                for (int i = 0; i < strSvcParamsParts.Length; i += 2)\n                                {\n                                    DnsSvcParamKey svcParamKey = Enum.Parse<DnsSvcParamKey>(strSvcParamsParts[i].Replace('-', '_'), true);\n                                    DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]);\n\n                                    svcParams.Add(svcParamKey, svcParamValue);\n                                }\n                            }\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSVCBRecordData(svcPriority, targetName, svcParams));\n\n                            if (autoIpv4Hint)\n                                newRecord.GetAuthSVCBRecordInfo().AutoIpv4Hint = true;\n\n                            if (autoIpv6Hint)\n                                newRecord.GetAuthSVCBRecordInfo().AutoIpv6Hint = true;\n\n                            if (autoIpv4Hint || autoIpv6Hint)\n                                ResolveSvcbAutoHints(zoneInfo.Name, newRecord, autoIpv4Hint, autoIpv6Hint, svcParams);\n                        }\n                        break;\n\n                    case DnsResourceRecordType.URI:\n                        {\n                            ushort priority = request.GetQueryOrForm(\"uriPriority\", ushort.Parse);\n                            ushort weight = request.GetQueryOrForm(\"uriWeight\", ushort.Parse);\n                            Uri uri = request.GetQueryOrForm(\"uri\", delegate (string value) { return new Uri(value); });\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsURIRecordData(priority, weight, uri));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CAA:\n                        {\n                            byte flags = request.GetQueryOrForm(\"flags\", byte.Parse);\n                            string tag = request.GetQueryOrForm(\"tag\");\n                            string value = request.GetQueryOrForm(\"value\");\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCAARecordData(flags, tag, value));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.ANAME:\n                        {\n                            string aname = request.GetQueryOrFormAlt(\"aname\", \"value\").Trim('.');\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsANAMERecordData(aname));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.FWD:\n                        {\n                            DnsTransportProtocol protocol = request.GetQueryOrFormEnum(\"protocol\", DnsTransportProtocol.Udp);\n                            string forwarder = request.GetQueryOrFormAlt(\"forwarder\", \"value\");\n                            bool dnssecValidation = request.GetQueryOrForm(\"dnssecValidation\", bool.Parse, false);\n\n                            DnsForwarderRecordProxyType proxyType = DnsForwarderRecordProxyType.DefaultProxy;\n                            string proxyAddress = null;\n                            ushort proxyPort = 0;\n                            string proxyUsername = null;\n                            string proxyPassword = null;\n\n                            if (!forwarder.Equals(\"this-server\"))\n                            {\n                                proxyType = request.GetQueryOrFormEnum(\"proxyType\", DnsForwarderRecordProxyType.DefaultProxy);\n                                switch (proxyType)\n                                {\n                                    case DnsForwarderRecordProxyType.Http:\n                                    case DnsForwarderRecordProxyType.Socks5:\n                                        proxyAddress = request.GetQueryOrForm(\"proxyAddress\");\n                                        proxyPort = request.GetQueryOrForm(\"proxyPort\", ushort.Parse);\n                                        proxyUsername = request.QueryOrForm(\"proxyUsername\");\n                                        proxyPassword = request.QueryOrForm(\"proxyPassword\");\n                                        break;\n                                }\n                            }\n\n                            byte priority = request.GetQueryOrForm(\"forwarderPriority\", byte.Parse, byte.MinValue);\n\n                            if (protocol == DnsTransportProtocol.Quic)\n                                DnsWebService.ValidateQuicSupport();\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, priority));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.APP:\n                        {\n                            if (!overwrite)\n                            {\n                                IReadOnlyList<DnsResourceRecord> existingRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneInfo.Name, domain, type);\n                                if (existingRecords.Count > 0)\n                                    throw new DnsWebServiceException(\"Record already exists. Use overwrite option if you wish to overwrite existing record.\");\n                            }\n\n                            string appName = request.GetQueryOrFormAlt(\"appName\", \"value\");\n                            string classPath = request.GetQueryOrForm(\"classPath\");\n                            string recordData = request.GetQueryOrForm(\"recordData\", \"\");\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsApplicationRecordData(appName, classPath, recordData));\n\n                            overwrite = true; //force SetRecord\n                        }\n                        break;\n\n                    default:\n                        {\n                            string strRData = request.GetQueryOrForm(\"rdata\");\n\n                            byte[] rdata;\n\n                            if (strRData.Contains(':'))\n                                rdata = strRData.ParseColonHexString();\n                            else\n                                rdata = Convert.FromHexString(strRData);\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, DnsResourceRecord.ReadRecordDataFrom(type, rdata));\n                        }\n                        break;\n                }\n\n                //update record info\n                GenericRecordInfo recordInfo = newRecord.GetAuthGenericRecordInfo();\n\n                recordInfo.LastModified = DateTime.UtcNow;\n                recordInfo.ExpiryTtl = expiryTtl;\n\n                if (!string.IsNullOrEmpty(comments))\n                    recordInfo.Comments = comments;\n\n                //add record\n                if (overwrite)\n                {\n                    _dnsWebService._dnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord);\n                }\n                else\n                {\n                    if (!_dnsWebService._dnsServer.AuthZoneManager.AddRecord(zoneInfo.Name, newRecord))\n                        throw new DnsWebServiceException(\"Cannot add record: record already exists.\");\n                }\n\n                //additional processing\n                if ((type == DnsResourceRecordType.A) || (type == DnsResourceRecordType.AAAA))\n                {\n                    bool updateSvcbHints = request.GetQueryOrForm(\"updateSvcbHints\", bool.Parse, false);\n                    if (updateSvcbHints)\n                        UpdateSvcbAutoHints(zoneInfo.Name, domain, type == DnsResourceRecordType.A, type == DnsResourceRecordType.AAAA);\n                }\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] New record was added to \" + zoneInfo.TypeName + \" zone '\" + zoneInfo.DisplayName + \"' successfully {record: \" + newRecord.ToString() + \"}\");\n\n                //save zone\n                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"zone\");\n                WriteZoneInfoAsJson(zoneInfo, jsonWriter);\n\n                jsonWriter.WritePropertyName(\"addedRecord\");\n                WriteRecordAsJson(newRecord, jsonWriter, true, null);\n            }\n\n            public void GetRecords(HttpContext context)\n            {\n                HttpRequest request = context.Request;\n\n                string domain = request.GetQueryOrForm(\"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                string zoneName = request.QueryOrForm(\"zone\");\n                if (zoneName is not null)\n                {\n                    zoneName = zoneName.Trim('.');\n\n                    if (DnsClient.IsDomainNameUnicode(zoneName))\n                        zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n                }\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + domain);\n\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                bool listZone = request.GetQueryOrForm(\"listZone\", bool.Parse, false);\n\n                List<DnsResourceRecord> records = new List<DnsResourceRecord>();\n\n                if (listZone)\n                    _dnsWebService._dnsServer.AuthZoneManager.ListAllZoneRecords(zoneInfo.Name, records);\n                else\n                    _dnsWebService._dnsServer.AuthZoneManager.ListAllRecords(zoneInfo.Name, domain, records);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"zone\");\n                WriteZoneInfoAsJson(zoneInfo, jsonWriter);\n\n                WriteRecordsAsJson(records, jsonWriter, true, zoneInfo);\n            }\n\n            public void DeleteRecord(HttpContext context)\n            {\n                HttpRequest request = context.Request;\n\n                string domain = request.GetQueryOrForm(\"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                string zoneName = request.QueryOrForm(\"zone\");\n                if (zoneName is not null)\n                {\n                    zoneName = zoneName.Trim('.');\n\n                    if (DnsClient.IsDomainNameUnicode(zoneName))\n                        zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n                }\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + domain);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                DnsResourceRecordType type = request.GetQueryOrFormEnum<DnsResourceRecordType>(\"type\");\n                switch (type)\n                {\n                    case DnsResourceRecordType.A:\n                    case DnsResourceRecordType.AAAA:\n                        {\n                            IPAddress ipAddress = IPAddress.Parse(request.GetQueryOrFormAlt(\"ipAddress\", \"value\"));\n\n                            if (type == DnsResourceRecordType.A)\n                            {\n                                if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsARecordData(ipAddress)))\n                                    throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                            }\n                            else\n                            {\n                                if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsAAAARecordData(ipAddress)))\n                                    throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                            }\n\n                            string ptrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128);\n                            AuthZoneInfo reverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain);\n                            if ((reverseZoneInfo is not null) && !reverseZoneInfo.Internal && ((reverseZoneInfo.Type == AuthZoneType.Primary) || (reverseZoneInfo.Type == AuthZoneType.Forwarder)))\n                            {\n                                IReadOnlyList<DnsResourceRecord> ptrRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(reverseZoneInfo.Name, ptrDomain, DnsResourceRecordType.PTR);\n                                if (ptrRecords.Count > 0)\n                                {\n                                    foreach (DnsResourceRecord ptrRecord in ptrRecords)\n                                    {\n                                        if ((ptrRecord.RDATA as DnsPTRRecordData).Domain.Equals(domain, StringComparison.OrdinalIgnoreCase))\n                                        {\n                                            //delete PTR record and save reverse zone\n                                            _dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(reverseZoneInfo.Name, ptrDomain, DnsResourceRecordType.PTR, ptrRecord.RDATA);\n                                            _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name);\n                                            break;\n                                        }\n                                    }\n                                }\n                            }\n\n                            bool updateSvcbHints = request.GetQueryOrForm(\"updateSvcbHints\", bool.Parse, false);\n                            if (updateSvcbHints)\n                                UpdateSvcbAutoHints(zoneInfo.Name, domain, type == DnsResourceRecordType.A, type == DnsResourceRecordType.AAAA);\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NS:\n                        {\n                            if ((zoneInfo.Type == AuthZoneType.Primary) && zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName))\n                                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.\");\n\n                            string nameServer = request.GetQueryOrFormAlt(\"nameServer\", \"value\").Trim('.');\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsNSRecordData(nameServer, false)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CNAME:\n                        if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, domain, type))\n                            throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n\n                        break;\n\n                    case DnsResourceRecordType.PTR:\n                        {\n                            string ptrName = request.GetQueryOrFormAlt(\"ptrName\", \"value\").Trim('.');\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsPTRRecordData(ptrName)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.MX:\n                        {\n                            ushort preference = request.GetQueryOrForm(\"preference\", ushort.Parse);\n                            string exchange = request.GetQueryOrFormAlt(\"exchange\", \"value\").Trim('.');\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsMXRecordData(preference, exchange)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TXT:\n                        {\n                            string text = request.GetQueryOrFormAlt(\"text\", \"value\");\n                            bool splitText = request.GetQueryOrForm(\"splitText\", bool.Parse, false);\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, splitText ? new DnsTXTRecordData(DecodeCharacterStrings(text)) : new DnsTXTRecordData(text)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.RP:\n                        {\n                            string mailbox = request.GetQueryOrForm(\"mailbox\", \"\").Trim('.');\n                            string txtDomain = request.GetQueryOrForm(\"txtDomain\", \"\").Trim('.');\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsRPRecordData(mailbox, txtDomain)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SRV:\n                        {\n                            ushort priority = request.GetQueryOrForm(\"priority\", ushort.Parse);\n                            ushort weight = request.GetQueryOrForm(\"weight\", ushort.Parse);\n                            ushort port = request.GetQueryOrForm(\"port\", ushort.Parse);\n                            string target = request.GetQueryOrFormAlt(\"target\", \"value\").Trim('.');\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSRVRecordData(priority, weight, port, target)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NAPTR:\n                        {\n                            ushort order = request.GetQueryOrForm(\"naptrOrder\", ushort.Parse);\n                            ushort preference = request.GetQueryOrForm(\"naptrPreference\", ushort.Parse);\n                            string flags = request.GetQueryOrForm(\"naptrFlags\", \"\");\n                            string services = request.GetQueryOrForm(\"naptrServices\", \"\");\n                            string regexp = request.GetQueryOrForm(\"naptrRegexp\", \"\");\n                            string replacement = request.GetQueryOrForm(\"naptrReplacement\", \"\").Trim('.');\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsNAPTRRecordData(order, preference, flags, services, regexp, replacement)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DNAME:\n                        if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, domain, type))\n                            throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n\n                        break;\n\n                    case DnsResourceRecordType.DS:\n                        {\n                            ushort keyTag = request.GetQueryOrForm(\"keyTag\", ushort.Parse);\n                            DnssecAlgorithm algorithm = Enum.Parse<DnssecAlgorithm>(request.GetQueryOrForm(\"algorithm\").Replace('-', '_'), true);\n                            DnssecDigestType digestType = Enum.Parse<DnssecDigestType>(request.GetQueryOrForm(\"digestType\").Replace('-', '_'), true);\n                            byte[] digest = Convert.FromHexString(request.GetQueryOrFormAlt(\"digest\", \"value\"));\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsDSRecordData(keyTag, algorithm, digestType, digest)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SSHFP:\n                        {\n                            DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum<DnsSSHFPAlgorithm>(\"sshfpAlgorithm\");\n                            DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum<DnsSSHFPFingerprintType>(\"sshfpFingerprintType\");\n                            byte[] sshfpFingerprint = request.GetQueryOrForm(\"sshfpFingerprint\", Convert.FromHexString);\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TLSA:\n                        {\n                            DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse<DnsTLSACertificateUsage>(request.GetQueryOrForm(\"tlsaCertificateUsage\").Replace('-', '_'), true);\n                            DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum<DnsTLSASelector>(\"tlsaSelector\");\n                            DnsTLSAMatchingType tlsaMatchingType = Enum.Parse<DnsTLSAMatchingType>(request.GetQueryOrForm(\"tlsaMatchingType\").Replace('-', '_'), true);\n                            string tlsaCertificateAssociationData = request.GetQueryOrForm(\"tlsaCertificateAssociationData\");\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SVCB:\n                    case DnsResourceRecordType.HTTPS:\n                        {\n                            ushort svcPriority = request.GetQueryOrForm(\"svcPriority\", ushort.Parse);\n                            string targetName = request.GetQueryOrForm(\"svcTargetName\").Trim('.');\n                            string strSvcParams = request.GetQueryOrForm(\"svcParams\");\n\n                            Dictionary<DnsSvcParamKey, DnsSvcParamValue> svcParams;\n\n                            if (strSvcParams.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                svcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(0);\n                            }\n                            else\n                            {\n                                string[] strSvcParamsParts = strSvcParams.Split('|');\n                                svcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(strSvcParamsParts.Length / 2);\n\n                                for (int i = 0; i < strSvcParamsParts.Length; i += 2)\n                                {\n                                    DnsSvcParamKey svcParamKey = Enum.Parse<DnsSvcParamKey>(strSvcParamsParts[i].Replace('-', '_'), true);\n                                    DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]);\n\n                                    svcParams.Add(svcParamKey, svcParamValue);\n                                }\n                            }\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSVCBRecordData(svcPriority, targetName, svcParams)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.URI:\n                        {\n                            ushort priority = request.GetQueryOrForm(\"uriPriority\", ushort.Parse);\n                            ushort weight = request.GetQueryOrForm(\"uriWeight\", ushort.Parse);\n                            Uri uri = request.GetQueryOrForm(\"uri\", delegate (string value) { return new Uri(value); });\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsURIRecordData(priority, weight, uri)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CAA:\n                        {\n                            byte flags = request.GetQueryOrForm(\"flags\", byte.Parse);\n                            string tag = request.GetQueryOrForm(\"tag\");\n                            string value = request.GetQueryOrForm(\"value\");\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsCAARecordData(flags, tag, value)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.ANAME:\n                        {\n                            string aname = request.GetQueryOrFormAlt(\"aname\", \"value\").Trim('.');\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsANAMERecordData(aname)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.FWD:\n                        {\n                            DnsTransportProtocol protocol = request.GetQueryOrFormEnum(\"protocol\", DnsTransportProtocol.Udp);\n                            string forwarder = request.GetQueryOrFormAlt(\"forwarder\", \"value\");\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, DnsForwarderRecordData.CreatePartialRecordData(protocol, forwarder)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n\n                    case DnsResourceRecordType.APP:\n                        if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, domain, type))\n                            throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n\n                        break;\n\n                    default:\n                        {\n                            string strRData = request.GetQueryOrForm(\"rdata\", string.Empty);\n\n                            byte[] rdata;\n\n                            if (strRData.Contains(':'))\n                                rdata = strRData.ParseColonHexString();\n                            else\n                                rdata = Convert.FromHexString(strRData);\n\n                            if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsUnknownRecordData(rdata)))\n                                throw new DnsWebServiceException(\"Cannot delete record: no such record exists.\");\n                        }\n                        break;\n                }\n\n                _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), \"[\" + sessionUser.Username + \"] Record was deleted from \" + zoneInfo.TypeName + \" zone '\" + zoneInfo.DisplayName + \"' successfully {domain: \" + domain + \"; type: \" + type + \";}\");\n\n                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);\n            }\n\n            public void UpdateRecord(HttpContext context)\n            {\n                HttpRequest request = context.Request;\n\n                string domain = request.GetQueryOrForm(\"domain\").Trim('.');\n\n                if (DnsClient.IsDomainNameUnicode(domain))\n                    domain = DnsClient.ConvertDomainNameToAscii(domain);\n\n                string zoneName = request.QueryOrForm(\"zone\");\n                if (zoneName is not null)\n                {\n                    zoneName = zoneName.Trim('.');\n\n                    if (DnsClient.IsDomainNameUnicode(zoneName))\n                        zoneName = DnsClient.ConvertDomainNameToAscii(zoneName);\n                }\n\n                AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName);\n                if (zoneInfo is null)\n                    throw new DnsWebServiceException(\"No such zone was found: \" + domain);\n\n                if (zoneInfo.Internal)\n                    throw new DnsWebServiceException(\"Access was denied to manage internal DNS Server zone.\");\n\n                User sessionUser = _dnsWebService.GetSessionUser(context);\n\n                if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify))\n                    throw new DnsWebServiceException(\"Access was denied.\");\n\n                string newDomain = request.GetQueryOrForm(\"newDomain\", domain).Trim('.');\n\n                DnsResourceRecordType type = request.GetQueryOrFormEnum<DnsResourceRecordType>(\"type\");\n\n                uint defaultTtl;\n\n                switch (type)\n                {\n                    case DnsResourceRecordType.NS:\n                        defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl;\n                        break;\n\n                    case DnsResourceRecordType.SOA:\n                        defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl;\n                        break;\n\n                    default:\n                        defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl;\n                        break;\n                }\n\n                uint ttl = request.GetQueryOrForm(\"ttl\", ZoneFile.ParseTtl, defaultTtl);\n\n                bool disable = request.GetQueryOrForm(\"disable\", bool.Parse, false);\n                string comments = request.QueryOrForm(\"comments\");\n                uint expiryTtl = request.GetQueryOrForm(\"expiryTtl\", ZoneFile.ParseTtl, 0u);\n\n                DnsResourceRecord oldRecord = null;\n                DnsResourceRecord newRecord;\n\n                switch (type)\n                {\n                    case DnsResourceRecordType.A:\n                    case DnsResourceRecordType.AAAA:\n                        {\n                            IPAddress ipAddress = IPAddress.Parse(request.GetQueryOrFormAlt(\"ipAddress\", \"value\"));\n                            IPAddress newIpAddress = IPAddress.Parse(request.GetQueryOrFormAlt(\"newIpAddress\", \"newValue\", ipAddress.ToString()));\n\n                            bool ptr = request.GetQueryOrForm(\"ptr\", bool.Parse, false);\n                            if (ptr)\n                            {\n                                string newPtrDomain = Zone.GetReverseZone(newIpAddress, type == DnsResourceRecordType.A ? 32 : 128);\n\n                                AuthZoneInfo newReverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(newPtrDomain);\n                                if (newReverseZoneInfo is null)\n                                {\n                                    bool createPtrZone = request.GetQueryOrForm(\"createPtrZone\", bool.Parse, false);\n                                    if (!createPtrZone)\n                                        throw new DnsWebServiceException(\"No reverse zone available to add PTR record.\");\n\n                                    string ptrZone = Zone.GetReverseZone(newIpAddress, type == DnsResourceRecordType.A ? 24 : 64);\n\n                                    newReverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(ptrZone);\n                                    if (newReverseZoneInfo is null)\n                                        throw new DnsWebServiceException(\"Failed to create reverse zone to add PTR record: \" + ptrZone);\n\n                                    //set permissions\n                                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete);\n                                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                                    _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);\n                                    _dnsWebService._authManager.SaveConfigFile();\n                                }\n\n                                if (newReverseZoneInfo.Internal)\n                                    throw new DnsWebServiceException(\"Reverse zone '\" + newReverseZoneInfo.DisplayName + \"' is an internal zone.\");\n\n                                if ((newReverseZoneInfo.Type != AuthZoneType.Primary) && (newReverseZoneInfo.Type != AuthZoneType.Forwarder))\n                                    throw new DnsWebServiceException(\"Reverse zone '\" + newReverseZoneInfo.DisplayName + \"' is not a primary or forwarder zone.\");\n\n                                string oldPtrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128);\n\n                                AuthZoneInfo oldReverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(oldPtrDomain);\n                                if ((oldReverseZoneInfo is not null) && !oldReverseZoneInfo.Internal && ((oldReverseZoneInfo.Type == AuthZoneType.Primary) || (oldReverseZoneInfo.Type == AuthZoneType.Forwarder)))\n                                {\n                                    //delete old PTR record if any and save old reverse zone\n                                    _dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(oldReverseZoneInfo.Name, oldPtrDomain, DnsResourceRecordType.PTR);\n                                    _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(oldReverseZoneInfo.Name);\n                                }\n\n                                //add new PTR record and save reverse zone\n                                DnsResourceRecord ptrRecord = new DnsResourceRecord(newPtrDomain, DnsResourceRecordType.PTR, DnsClass.IN, ttl, new DnsPTRRecordData(domain));\n                                ptrRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;\n                                ptrRecord.GetAuthGenericRecordInfo().ExpiryTtl = expiryTtl;\n\n                                _dnsWebService._dnsServer.AuthZoneManager.SetRecord(newReverseZoneInfo.Name, ptrRecord);\n                                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(newReverseZoneInfo.Name);\n                            }\n\n                            if (type == DnsResourceRecordType.A)\n                            {\n                                oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsARecordData(ipAddress));\n                                newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsARecordData(newIpAddress));\n                            }\n                            else\n                            {\n                                oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsAAAARecordData(ipAddress));\n                                newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsAAAARecordData(newIpAddress));\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NS:\n                        {\n                            string nameServer = request.GetQueryOrFormAlt(\"nameServer\", \"value\").Trim('.');\n                            string newNameServer = request.GetQueryOrFormAlt(\"newNameServer\", \"newValue\", nameServer).Trim('.');\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsNSRecordData(nameServer));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsNSRecordData(newNameServer));\n\n                            if (request.TryGetQueryOrForm(\"glue\", out string glueAddresses))\n                            {\n                                if (zoneInfo.Name.Equals(newDomain, StringComparison.OrdinalIgnoreCase) && (newNameServer.Equals(newDomain, StringComparison.OrdinalIgnoreCase) || newNameServer.EndsWith(\".\" + newDomain, StringComparison.OrdinalIgnoreCase)))\n                                    throw new DnsWebServiceException(\"The zone's own NS records cannot have glue addresses. Please add separate A/AAAA records in the zone instead.\");\n\n                                newRecord.SetGlueRecords(glueAddresses);\n                            }\n\n                            if ((zoneInfo.Type == AuthZoneType.Primary) && zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName))\n                            {\n                                if (disable)\n                                    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.\");\n\n                                if (expiryTtl > 0)\n                                    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.\");\n\n                                if (!nameServer.Equals(newNameServer, StringComparison.OrdinalIgnoreCase))\n                                    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.\");\n\n                                if (!string.IsNullOrEmpty(glueAddresses))\n                                    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.\");\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CNAME:\n                        {\n                            string cname = request.GetQueryOrFormAlt(\"cname\", \"value\").Trim('.');\n\n                            if (cname.Equals(newDomain, StringComparison.OrdinalIgnoreCase))\n                                throw new DnsWebServiceException(\"CNAME domain name cannot be same as that of the record name.\");\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCNAMERecordData(cname));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SOA:\n                        {\n                            string primaryNameServer = request.GetQueryOrForm(\"primaryNameServer\").Trim('.');\n                            string responsiblePerson = request.GetQueryOrForm(\"responsiblePerson\").Trim('.');\n                            uint serial = request.GetQueryOrForm(\"serial\", uint.Parse);\n                            uint refresh = request.GetQueryOrForm(\"refresh\", ZoneFile.ParseTtl);\n                            uint retry = request.GetQueryOrForm(\"retry\", ZoneFile.ParseTtl);\n                            uint expire = request.GetQueryOrForm(\"expire\", ZoneFile.ParseTtl);\n                            uint minimum = request.GetQueryOrForm(\"minimum\", ZoneFile.ParseTtl);\n\n                            if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName))\n                            {\n                                if (!primaryNameServer.Equals(_dnsWebService._dnsServer.ServerDomain, StringComparison.OrdinalIgnoreCase))\n                                    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.\");\n                            }\n\n                            newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSOARecordData(primaryNameServer, responsiblePerson, serial, refresh, retry, expire, minimum));\n\n                            switch (zoneInfo.Type)\n                            {\n                                case AuthZoneType.Primary:\n                                case AuthZoneType.Forwarder:\n                                case AuthZoneType.Catalog:\n                                    {\n                                        if (request.TryGetQueryOrForm(\"useSerialDateScheme\", bool.Parse, out bool useSerialDateScheme))\n                                            newRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme = useSerialDateScheme;\n                                    }\n                                    break;\n                            }\n                        }\n                        break;\n\n                    case DnsResourceRecordType.PTR:\n                        {\n                            string ptrName = request.GetQueryOrFormAlt(\"ptrName\", \"value\").Trim('.');\n                            string newPtrName = request.GetQueryOrFormAlt(\"newPtrName\", \"newValue\", ptrName).Trim('.');\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsPTRRecordData(ptrName));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsPTRRecordData(newPtrName));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.MX:\n                        {\n                            ushort preference = request.GetQueryOrForm(\"preference\", ushort.Parse);\n                            ushort newPreference = request.GetQueryOrForm(\"newPreference\", ushort.Parse, preference);\n\n                            string exchange = request.GetQueryOrFormAlt(\"exchange\", \"value\").Trim('.');\n                            string newExchange = request.GetQueryOrFormAlt(\"newExchange\", \"newValue\", exchange).Trim('.');\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsMXRecordData(preference, exchange));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsMXRecordData(newPreference, newExchange));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TXT:\n                        {\n                            string text = request.GetQueryOrFormAlt(\"text\", \"value\");\n                            string newText = request.GetQueryOrFormAlt(\"newText\", \"newValue\", text);\n\n                            bool splitText = request.GetQueryOrForm(\"splitText\", bool.Parse, false);\n                            bool newSplitText = request.GetQueryOrForm(\"newSplitText\", bool.Parse, splitText);\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, splitText ? new DnsTXTRecordData(DecodeCharacterStrings(text)) : new DnsTXTRecordData(text));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, newSplitText ? new DnsTXTRecordData(DecodeCharacterStrings(newText)) : new DnsTXTRecordData(newText));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.RP:\n                        {\n                            string mailbox = request.GetQueryOrForm(\"mailbox\", \"\").Trim('.');\n                            string newMailbox = request.GetQueryOrForm(\"newMailbox\", mailbox).Trim('.');\n\n                            string txtDomain = request.GetQueryOrForm(\"txtDomain\", \"\").Trim('.');\n                            string newTxtDomain = request.GetQueryOrForm(\"newTxtDomain\", txtDomain).Trim('.');\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsRPRecordData(mailbox, txtDomain));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsRPRecordData(newMailbox, newTxtDomain));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SRV:\n                        {\n                            ushort priority = request.GetQueryOrForm(\"priority\", ushort.Parse);\n                            ushort newPriority = request.GetQueryOrForm(\"newPriority\", ushort.Parse, priority);\n\n                            ushort weight = request.GetQueryOrForm(\"weight\", ushort.Parse);\n                            ushort newWeight = request.GetQueryOrForm(\"newWeight\", ushort.Parse, weight);\n\n                            ushort port = request.GetQueryOrForm(\"port\", ushort.Parse);\n                            ushort newPort = request.GetQueryOrForm(\"newPort\", ushort.Parse, port);\n\n                            string target = request.GetQueryOrFormAlt(\"target\", \"value\").Trim('.');\n                            string newTarget = request.GetQueryOrFormAlt(\"newTarget\", \"newValue\", target).Trim('.');\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSRVRecordData(priority, weight, port, target));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSRVRecordData(newPriority, newWeight, newPort, newTarget));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.NAPTR:\n                        {\n                            ushort order = request.GetQueryOrForm(\"naptrOrder\", ushort.Parse);\n                            ushort newOrder = request.GetQueryOrForm(\"naptrNewOrder\", ushort.Parse, order);\n\n                            ushort preference = request.GetQueryOrForm(\"naptrPreference\", ushort.Parse);\n                            ushort newPreference = request.GetQueryOrForm(\"naptrNewPreference\", ushort.Parse, preference);\n\n                            string flags = request.GetQueryOrForm(\"naptrFlags\", \"\");\n                            string newFlags = request.GetQueryOrForm(\"naptrNewFlags\", flags);\n\n                            string services = request.GetQueryOrForm(\"naptrServices\", \"\");\n                            string newServices = request.GetQueryOrForm(\"naptrNewServices\", services);\n\n                            string regexp = request.GetQueryOrForm(\"naptrRegexp\", \"\");\n                            string newRegexp = request.GetQueryOrForm(\"naptrNewRegexp\", regexp);\n\n                            string replacement = request.GetQueryOrForm(\"naptrReplacement\", \"\").Trim('.');\n                            string newReplacement = request.GetQueryOrForm(\"naptrNewReplacement\", replacement).Trim('.');\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsNAPTRRecordData(order, preference, flags, services, regexp, replacement));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsNAPTRRecordData(newOrder, newPreference, newFlags, newServices, newRegexp, newReplacement));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DNAME:\n                        {\n                            string dname = request.GetQueryOrFormAlt(\"dname\", \"value\").Trim('.');\n\n                            if (dname.EndsWith(\".\" + newDomain, StringComparison.OrdinalIgnoreCase))\n                                throw new DnsWebServiceException(\"DNAME domain name cannot be a sub domain of the record name.\");\n\n                            if (dname.Equals(newDomain, StringComparison.OrdinalIgnoreCase))\n                                throw new DnsWebServiceException(\"DNAME domain name cannot be same as that of the record name.\");\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDNAMERecordData(dname));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.DS:\n                        {\n                            ushort keyTag = request.GetQueryOrForm(\"keyTag\", ushort.Parse);\n                            ushort newKeyTag = request.GetQueryOrForm(\"newKeyTag\", ushort.Parse, keyTag);\n\n                            DnssecAlgorithm algorithm = Enum.Parse<DnssecAlgorithm>(request.GetQueryOrForm(\"algorithm\").Replace('-', '_'), true);\n                            DnssecAlgorithm newAlgorithm = Enum.Parse<DnssecAlgorithm>(request.GetQueryOrForm(\"newAlgorithm\", algorithm.ToString()).Replace('-', '_'), true);\n\n                            DnssecDigestType digestType = Enum.Parse<DnssecDigestType>(request.GetQueryOrForm(\"digestType\").Replace('-', '_'), true);\n                            DnssecDigestType newDigestType = Enum.Parse<DnssecDigestType>(request.GetQueryOrForm(\"newDigestType\", digestType.ToString()).Replace('-', '_'), true);\n\n                            byte[] digest = request.GetQueryOrFormAlt(\"digest\", \"value\", Convert.FromHexString);\n                            byte[] newDigest = request.GetQueryOrFormAlt(\"newDigest\", \"newValue\", Convert.FromHexString, digest);\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDSRecordData(keyTag, algorithm, digestType, digest));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDSRecordData(newKeyTag, newAlgorithm, newDigestType, newDigest));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SSHFP:\n                        {\n                            DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum<DnsSSHFPAlgorithm>(\"sshfpAlgorithm\");\n                            DnsSSHFPAlgorithm newSshfpAlgorithm = request.GetQueryOrFormEnum(\"newSshfpAlgorithm\", sshfpAlgorithm);\n\n                            DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum<DnsSSHFPFingerprintType>(\"sshfpFingerprintType\");\n                            DnsSSHFPFingerprintType newSshfpFingerprintType = request.GetQueryOrFormEnum(\"newSshfpFingerprintType\", sshfpFingerprintType);\n\n                            byte[] sshfpFingerprint = request.GetQueryOrForm(\"sshfpFingerprint\", Convert.FromHexString);\n                            byte[] newSshfpFingerprint = request.GetQueryOrForm(\"newSshfpFingerprint\", Convert.FromHexString, sshfpFingerprint);\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(newSshfpAlgorithm, newSshfpFingerprintType, newSshfpFingerprint));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.TLSA:\n                        {\n                            DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse<DnsTLSACertificateUsage>(request.GetQueryOrForm(\"tlsaCertificateUsage\").Replace('-', '_'), true);\n                            DnsTLSACertificateUsage newTlsaCertificateUsage = Enum.Parse<DnsTLSACertificateUsage>(request.GetQueryOrForm(\"newTlsaCertificateUsage\", tlsaCertificateUsage.ToString()).Replace('-', '_'), true);\n\n                            DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum<DnsTLSASelector>(\"tlsaSelector\");\n                            DnsTLSASelector newTlsaSelector = request.GetQueryOrFormEnum(\"newTlsaSelector\", tlsaSelector);\n\n                            DnsTLSAMatchingType tlsaMatchingType = Enum.Parse<DnsTLSAMatchingType>(request.GetQueryOrForm(\"tlsaMatchingType\").Replace('-', '_'), true);\n                            DnsTLSAMatchingType newTlsaMatchingType = Enum.Parse<DnsTLSAMatchingType>(request.GetQueryOrForm(\"newTlsaMatchingType\", tlsaMatchingType.ToString()).Replace('-', '_'), true);\n\n                            string tlsaCertificateAssociationData = request.GetQueryOrForm(\"tlsaCertificateAssociationData\");\n                            string newTlsaCertificateAssociationData = request.GetQueryOrForm(\"newTlsaCertificateAssociationData\", tlsaCertificateAssociationData);\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsTLSARecordData(newTlsaCertificateUsage, newTlsaSelector, newTlsaMatchingType, newTlsaCertificateAssociationData));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.SVCB:\n                    case DnsResourceRecordType.HTTPS:\n                        {\n                            ushort svcPriority = request.GetQueryOrForm(\"svcPriority\", ushort.Parse);\n                            ushort newSvcPriority = request.GetQueryOrForm(\"newSvcPriority\", ushort.Parse, svcPriority);\n\n                            string targetName = request.GetQueryOrForm(\"svcTargetName\").Trim('.');\n                            string newTargetName = request.GetQueryOrForm(\"newSvcTargetName\", targetName).Trim('.');\n\n                            string strSvcParams = request.GetQueryOrForm(\"svcParams\");\n                            string strNewSvcParams = request.GetQueryOrForm(\"newSvcParams\", strSvcParams);\n\n                            bool autoIpv4Hint = request.GetQueryOrForm(\"autoIpv4Hint\", bool.Parse, false);\n                            bool autoIpv6Hint = request.GetQueryOrForm(\"autoIpv6Hint\", bool.Parse, false);\n\n                            Dictionary<DnsSvcParamKey, DnsSvcParamValue> svcParams;\n\n                            if (strSvcParams.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                svcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(0);\n                            }\n                            else\n                            {\n                                string[] strSvcParamsParts = strSvcParams.Split('|');\n                                svcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(strSvcParamsParts.Length / 2);\n\n                                for (int i = 0; i < strSvcParamsParts.Length; i += 2)\n                                {\n                                    DnsSvcParamKey svcParamKey = Enum.Parse<DnsSvcParamKey>(strSvcParamsParts[i].Replace('-', '_'), true);\n                                    DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]);\n\n                                    svcParams.Add(svcParamKey, svcParamValue);\n                                }\n                            }\n\n                            Dictionary<DnsSvcParamKey, DnsSvcParamValue> newSvcParams;\n\n                            if (strNewSvcParams.Equals(\"false\", StringComparison.OrdinalIgnoreCase))\n                            {\n                                newSvcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(0);\n                            }\n                            else\n                            {\n                                string[] strSvcParamsParts = strNewSvcParams.Split('|');\n                                newSvcParams = new Dictionary<DnsSvcParamKey, DnsSvcParamValue>(strSvcParamsParts.Length / 2);\n\n                                for (int i = 0; i < strSvcParamsParts.Length; i += 2)\n                                {\n                                    DnsSvcParamKey svcParamKey = Enum.Parse<DnsSvcParamKey>(strSvcParamsParts[i].Replace('-', '_'), true);\n                                    DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]);\n\n                                    newSvcParams.Add(svcParamKey, svcParamValue);\n                                }\n                            }\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSVCBRecordData(svcPriority, targetName, svcParams));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSVCBRecordData(newSvcPriority, newTargetName, newSvcParams));\n\n                            if (autoIpv4Hint)\n                                newRecord.GetAuthSVCBRecordInfo().AutoIpv4Hint = true;\n\n                            if (autoIpv6Hint)\n                                newRecord.GetAuthSVCBRecordInfo().AutoIpv6Hint = true;\n\n                            if (autoIpv4Hint || autoIpv6Hint)\n                                ResolveSvcbAutoHints(zoneInfo.Name, newRecord, autoIpv4Hint, autoIpv6Hint, newSvcParams);\n                        }\n                        break;\n\n                    case DnsResourceRecordType.URI:\n                        {\n                            ushort priority = request.GetQueryOrForm(\"uriPriority\", ushort.Parse);\n                            ushort newPriority = request.GetQueryOrForm(\"newUriPriority\", ushort.Parse, priority);\n\n                            ushort weight = request.GetQueryOrForm(\"uriWeight\", ushort.Parse);\n                            ushort newWeight = request.GetQueryOrForm(\"newUriWeight\", ushort.Parse, weight);\n\n                            Uri uri = request.GetQueryOrForm(\"uri\", delegate (string value) { return new Uri(value); });\n                            Uri newUri = request.GetQueryOrForm(\"newUri\", delegate (string value) { return new Uri(value); }, uri);\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsURIRecordData(priority, weight, uri));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsURIRecordData(newPriority, newWeight, newUri));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.CAA:\n                        {\n                            byte flags = request.GetQueryOrForm(\"flags\", byte.Parse);\n                            byte newFlags = request.GetQueryOrForm(\"newFlags\", byte.Parse, flags);\n\n                            string tag = request.GetQueryOrForm(\"tag\");\n                            string newTag = request.GetQueryOrForm(\"newTag\", tag);\n\n                            string value = request.GetQueryOrForm(\"value\");\n                            string newValue = request.GetQueryOrForm(\"newValue\", value);\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCAARecordData(flags, tag, value));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCAARecordData(newFlags, newTag, newValue));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.ANAME:\n                        {\n                            string aname = request.GetQueryOrFormAlt(\"aname\", \"value\").Trim('.');\n                            string newAName = request.GetQueryOrFormAlt(\"newAName\", \"newValue\", aname).Trim('.');\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsANAMERecordData(aname));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsANAMERecordData(newAName));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.FWD:\n                        {\n                            DnsTransportProtocol protocol = request.GetQueryOrFormEnum(\"protocol\", DnsTransportProtocol.Udp);\n                            DnsTransportProtocol newProtocol = request.GetQueryOrFormEnum(\"newProtocol\", protocol);\n\n                            string forwarder = request.GetQueryOrFormAlt(\"forwarder\", \"value\");\n                            string newForwarder = request.GetQueryOrFormAlt(\"newForwarder\", \"newValue\", forwarder);\n\n                            bool dnssecValidation = request.GetQueryOrForm(\"dnssecValidation\", bool.Parse, false);\n\n                            DnsForwarderRecordProxyType proxyType = DnsForwarderRecordProxyType.DefaultProxy;\n                            string proxyAddress = null;\n                            ushort proxyPort = 0;\n                            string proxyUsername = null;\n                            string proxyPassword = null;\n\n                            if (!newForwarder.Equals(\"this-server\"))\n                            {\n                                proxyType = request.GetQueryOrFormEnum(\"proxyType\", DnsForwarderRecordProxyType.DefaultProxy);\n                                switch (proxyType)\n                                {\n                                    case DnsForwarderRecordProxyType.Http:\n                                    case DnsForwarderRecordProxyType.Socks5:\n                                        proxyAddress = request.GetQueryOrForm(\"proxyAddress\");\n                                        proxyPort = request.GetQueryOrForm(\"proxyPort\", ushort.Parse);\n                                        proxyUsername = request.QueryOrForm(\"proxyUsername\");\n                                        proxyPassword = request.QueryOrForm(\"proxyPassword\");\n                                        break;\n                                }\n                            }\n\n                            byte priority = request.GetQueryOrForm(\"forwarderPriority\", byte.Parse, byte.MinValue);\n\n                            if (newProtocol == DnsTransportProtocol.Quic)\n                                DnsWebService.ValidateQuicSupport();\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, DnsForwarderRecordData.CreatePartialRecordData(protocol, forwarder));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, 0, new DnsForwarderRecordData(newProtocol, newForwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, priority));\n                        }\n                        break;\n\n                    case DnsResourceRecordType.APP:\n                        {\n                            string appName = request.GetQueryOrFormAlt(\"appName\", \"value\");\n                            string classPath = request.GetQueryOrForm(\"classPath\");\n                            string recordData = request.GetQueryOrForm(\"recordData\", \"\");\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsApplicationRecordData(appName, classPath, recordData));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsApplicationRecordData(appName, classPath, recordData));\n                        }\n                        break;\n\n                    default:\n                        {\n                            string strRData = request.GetQueryOrForm(\"rdata\");\n                            string strNewRData = request.GetQueryOrForm(\"newRData\", strRData);\n\n                            byte[] rdata;\n\n                            if (strRData.Contains(':'))\n                                rdata = strRData.ParseColonHexString();\n                            else\n                                rdata = Convert.FromHexString(strRData);\n\n                            byte[] newRData;\n\n                            if (strNewRData.Contains(':'))\n                                newRData = strNewRData.ParseColonHexString();\n                            else\n                                newRData = Convert.FromHexString(strNewRData);\n\n                            oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsUnknownRecordData(rdata));\n                            newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsUnknownRecordData(newRData));\n                        }\n                        break;\n                }\n\n                //update record info\n                GenericRecordInfo recordInfo = newRecord.GetAuthGenericRecordInfo();\n\n                recordInfo.LastModified = DateTime.UtcNow;\n                recordInfo.ExpiryTtl = expiryTtl;\n                recordInfo.Disabled = disable;\n                recordInfo.Comments = comments;\n\n                //update record\n                if (type == DnsResourceRecordType.SOA)\n                {\n                    //special SOA case\n                    switch (zoneInfo.Type)\n                    {\n                        case AuthZoneType.Primary:\n                        case AuthZoneType.Forwarder:\n                        case AuthZoneType.Catalog:\n                            _dnsWebService._dnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord);\n                            break;\n                    }\n\n                    //get updated record to return json\n                    newRecord = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA)[0];\n                }\n                else\n                {\n                    _dnsWebService._dnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord);\n                }\n\n                //additional processing\n                if ((type == DnsResourceRecordType.A) || (type == DnsResourceRecordType.AAAA))\n                {\n                    bool updateSvcbHints = request.GetQueryOrForm(\"updateSvcbHints\", bool.Parse, false);\n                    if (updateSvcbHints)\n                        UpdateSvcbAutoHints(zoneInfo.Name, newDomain, type == DnsResourceRecordType.A, type == DnsResourceRecordType.AAAA);\n                }\n\n                _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() + \"}\");\n\n                //save zone\n                _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);\n\n                Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();\n\n                jsonWriter.WritePropertyName(\"zone\");\n                WriteZoneInfoAsJson(zoneInfo, jsonWriter);\n\n                jsonWriter.WritePropertyName(\"updatedRecord\");\n                WriteRecordAsJson(newRecord, jsonWriter, true, zoneInfo);\n            }\n\n            #endregion\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/dohwww/css/main.css",
    "content": "﻿\nhtml, body {\n    height: 100% !important;\n}\n\nbody {\n    margin: 0px !important;\n    line-height: 1.42857143 !important;\n}\n\na {\n    color: #6699ff;\n}\n\n    a:hover {\n        color: #6699ff;\n    }\n\n#content {\n    min-height: 100%;\n}\n\n.container {\n    margin-left: auto;\n    margin-right: auto;\n    padding: 55px 15px 60px 15px;\n    word-wrap: break-word;\n}\n\n    .container .pageLogin {\n        display: none;\n        margin: auto;\n        width: 500px;\n        padding: 150px 0 0 0;\n    }\n\n    .container .page {\n        display: none;\n    }\n\n.features {\n    margin: 80px auto 40px auto;\n    font-family: Arial;\n}\n\n    .features .pull-left {\n        width: 50%;\n    }\n\n    .features .pull-right {\n        width: 50%;\n    }\n\n    .features h3 {\n        font-size: 22px;\n        text-align: center;\n    }\n\n    .features p {\n        color: rgb(119, 119, 119);\n        text-align: center;\n    }\n\n    .features li {\n        color: rgb(119, 119, 119);\n    }\n\n.shadow-screenshot {\n    box-shadow: 2px 3px 15px 1px #888888;\n}\n\n.auto-resize-img {\n    max-width: 100%;\n    height: auto;\n    display: block;\n    margin-right: auto;\n    margin-left: auto;\n}\n\n@media (min-width: 992px) {\n    #header .title, .container, #footer .content {\n        width: 970px;\n    }\n}\n\n@media (min-width: 1200px) {\n    #header .title, .container, #footer .content {\n        width: 1170px;\n    }\n\n    .stats-panel .stats-item {\n        padding: 6px !important;\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/dohwww/index.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n    <title>Technitium DNS Server</title>\n\n    <link href=\"/css/main.css\" rel=\"stylesheet\" />\n    <link href=\"/css/bootstrap.min.css\" rel=\"stylesheet\" />\n\n    <script src=\"/js/jquery.min.js\"></script>\n    <script src=\"/js/main.js\"></script>\n</head>\n<body>\n    <div id=\"content\">\n        <div class=\"container\">\n            <div class=\"features\" style=\"text-align: center; margin-top: 60px;\">\n                <a href=\"/\">\n                    <img src=\"/img/logo.png\" alt=\"Technitium Logo\" />\n                </a>\n                <h1>Technitium DNS Server</h1>\n                <p style=\"max-width: 800px; margin-right: auto; margin-left: auto;\">This server supports encrypted DNS protocol (DNS-over-HTTPS) that you can use with your web browser like Mozilla Firefox.</p>\n\n                <h3 style=\"margin-top: 40px;\">The Encrypted DNS Service URL</h3>\n                <p>\n                    Use the following URL to configure your clients for consuming the DNS-over-HTTPS service.\n                </p>\n                <p style=\"font-size: 16px; font-weight: bold;\">\n                    <a id=\"lnkDoH\" href=\"\"></a>\n                </p>\n\n                <h3 style=\"margin-top: 40px;\">Mozilla Firefox Configuration</h3>\n                <p style=\"max-width: 800px; margin-right: auto; margin-left: auto;\">\n                    To configure Firefox, go to <b>Settings &gt; Privacy &amp; Security</b> and scroll down to find <b>DNS over HTTPS</b> section. Click on the <b>Max Protection</b> option, select <b>Custom</b> option in the <b>Choose provider</b> drop down box, and enter the encrypted DNS service URL given above.\n                </p>\n                <img class=\"auto-resize-img shadow-screenshot\" style=\"margin-top: 20px; margin-bottom: 20px;\" src=\"img/firefox-doh.png\" alt=\"Mozilla Firefox Custom DNS-over-HTTPS Option\" />\n                <p style=\"max-width: 800px; margin-right: auto; margin-left: auto;\">\n                    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.\n                </p>\n            </div>\n        </div>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "DnsServerCore/dohwww/js/main.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n$(function () {\n    var link = \"https://\" + window.location.hostname + \"/dns-query\";\n\n    var lnkDoH = $(\"#lnkDoH\");\n\n    lnkDoH.text(link);\n    lnkDoH.attr(\"href\", link);\n});"
  },
  {
    "path": "DnsServerCore/dohwww/robots.txt",
    "content": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "DnsServerCore/named.root",
    "content": ";       This file holds the information on root name servers needed to \n;       initialize cache of Internet domain name servers\n;       (e.g. reference this file in the \"cache  .  <file>\"\n;       configuration file of BIND domain name servers). \n; \n;       This file is made available by InterNIC \n;       under anonymous FTP as\n;           file                /domain/named.cache \n;           on server           FTP.INTERNIC.NET\n;       -OR-                    RS.INTERNIC.NET\n;\n;       last update:     October 29, 2025\n;       related version of root zone:     2025102901\n; \n; FORMERLY NS.INTERNIC.NET \n;\n.                        3600000      NS    A.ROOT-SERVERS.NET.\nA.ROOT-SERVERS.NET.      3600000      A     198.41.0.4\nA.ROOT-SERVERS.NET.      3600000      AAAA  2001:503:ba3e::2:30\n; \n; FORMERLY NS1.ISI.EDU \n;\n.                        3600000      NS    B.ROOT-SERVERS.NET.\nB.ROOT-SERVERS.NET.      3600000      A     170.247.170.2\nB.ROOT-SERVERS.NET.      3600000      AAAA  2801:1b8:10::b\n; \n; FORMERLY C.PSI.NET \n;\n.                        3600000      NS    C.ROOT-SERVERS.NET.\nC.ROOT-SERVERS.NET.      3600000      A     192.33.4.12\nC.ROOT-SERVERS.NET.      3600000      AAAA  2001:500:2::c\n; \n; FORMERLY TERP.UMD.EDU \n;\n.                        3600000      NS    D.ROOT-SERVERS.NET.\nD.ROOT-SERVERS.NET.      3600000      A     199.7.91.13\nD.ROOT-SERVERS.NET.      3600000      AAAA  2001:500:2d::d\n; \n; FORMERLY NS.NASA.GOV\n;\n.                        3600000      NS    E.ROOT-SERVERS.NET.\nE.ROOT-SERVERS.NET.      3600000      A     192.203.230.10\nE.ROOT-SERVERS.NET.      3600000      AAAA  2001:500:a8::e\n; \n; FORMERLY NS.ISC.ORG\n;\n.                        3600000      NS    F.ROOT-SERVERS.NET.\nF.ROOT-SERVERS.NET.      3600000      A     192.5.5.241\nF.ROOT-SERVERS.NET.      3600000      AAAA  2001:500:2f::f\n; \n; FORMERLY NS.NIC.DDN.MIL\n;\n.                        3600000      NS    G.ROOT-SERVERS.NET.\nG.ROOT-SERVERS.NET.      3600000      A     192.112.36.4\nG.ROOT-SERVERS.NET.      3600000      AAAA  2001:500:12::d0d\n; \n; FORMERLY AOS.ARL.ARMY.MIL\n;\n.                        3600000      NS    H.ROOT-SERVERS.NET.\nH.ROOT-SERVERS.NET.      3600000      A     198.97.190.53\nH.ROOT-SERVERS.NET.      3600000      AAAA  2001:500:1::53\n; \n; FORMERLY NIC.NORDU.NET\n;\n.                        3600000      NS    I.ROOT-SERVERS.NET.\nI.ROOT-SERVERS.NET.      3600000      A     192.36.148.17\nI.ROOT-SERVERS.NET.      3600000      AAAA  2001:7fe::53\n; \n; OPERATED BY VERISIGN, INC.\n;\n.                        3600000      NS    J.ROOT-SERVERS.NET.\nJ.ROOT-SERVERS.NET.      3600000      A     192.58.128.30\nJ.ROOT-SERVERS.NET.      3600000      AAAA  2001:503:c27::2:30\n; \n; OPERATED BY RIPE NCC\n;\n.                        3600000      NS    K.ROOT-SERVERS.NET.\nK.ROOT-SERVERS.NET.      3600000      A     193.0.14.129\nK.ROOT-SERVERS.NET.      3600000      AAAA  2001:7fd::1\n; \n; OPERATED BY ICANN\n;\n.                        3600000      NS    L.ROOT-SERVERS.NET.\nL.ROOT-SERVERS.NET.      3600000      A     199.7.83.42\nL.ROOT-SERVERS.NET.      3600000      AAAA  2001:500:9f::42\n; \n; OPERATED BY WIDE\n;\n.                        3600000      NS    M.ROOT-SERVERS.NET.\nM.ROOT-SERVERS.NET.      3600000      A     202.12.27.33\nM.ROOT-SERVERS.NET.      3600000      AAAA  2001:dc3::35\n; End of file"
  },
  {
    "path": "DnsServerCore/root-anchors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<TrustAnchor id=\"0C05FDD6-422C-4910-8ED6-430ED15E11C2\" source=\"http://data.iana.org/root-anchors/root-anchors.xml\">\n    <Zone>.</Zone>\n    <KeyDigest id=\"Kjqmt7v\" validFrom=\"2010-07-15T00:00:00+00:00\" validUntil=\"2019-01-11T00:00:00+00:00\">\n        <KeyTag>19036</KeyTag>\n        <Algorithm>8</Algorithm>\n        <DigestType>2</DigestType>\n        <Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>\n    </KeyDigest>\n    <KeyDigest id=\"Klajeyz\" validFrom=\"2017-02-02T00:00:00+00:00\">\n        <KeyTag>20326</KeyTag>\n        <Algorithm>8</Algorithm>\n        <DigestType>2</DigestType>\n        <Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>\n        <PublicKey>AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=</PublicKey>\n        <Flags>257</Flags>\n    </KeyDigest>\n    <KeyDigest id=\"Kmyv6jo\" validFrom=\"2024-07-18T00:00:00+00:00\">\n        <KeyTag>38696</KeyTag>\n        <Algorithm>8</Algorithm>\n        <DigestType>2</DigestType>\n        <Digest>683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16</Digest>\n        <PublicKey>AwEAAa96jeuknZlaeSrvyAJj6ZHv28hhOKkx3rLGXVaC6rXTsDc449/cidltpkyGwCJNnOAlFNKF2jBosZBU5eeHspaQWOmOElZsjICMQMC3aeHbGiShvZsx4wMYSjH8e7Vrhbu6irwCzVBApESjbUdpWWmEnhathWu1jo+siFUiRAAxm9qyJNg/wOZqqzL/dL/q8PkcRU5oUKEpUge71M3ej2/7CPqpdVwuMoTvoB+ZOT4YeGyxMvHmbrxlFzGOHOijtzN+u1TQNatX2XBuzZNQ1K+s2CXkPIZo7s6JgZyvaBevYtxPvYLw4z9mR7K2vaF18UYH9Z9GNUUeayffKC73PYc=</PublicKey>\n        <Flags>257</Flags>\n    </KeyDigest>\n</TrustAnchor>"
  },
  {
    "path": "DnsServerCore/www/css/main.css",
    "content": "﻿html, body {\n    height: 100% !important;\n    scrollbar-gutter: stable;\n}\n\n    body.modal-open {\n        padding-right: 0 !important;\n    }\n\nbody {\n    margin: 0px !important;\n    line-height: 1.42857143 !important;\n}\n\na {\n    color: #6699ff;\n}\n\n    a:hover {\n        color: #6699ff;\n    }\n\n    a:visited {\n        color: #6699ff;\n    }\n\nth a {\n    color: black !important;\n}\n\n    th a:hover {\n        color: black;\n        text-decoration: none;\n    }\n\n#header {\n    background-color: #6699ff;\n    height: 32px;\n    margin-bottom: -32px;\n    box-shadow: 0px 1px 15px 0px #888888;\n    width: 100%;\n    min-width: 970px;\n}\n\n    #header .title {\n        margin: 0 auto;\n        color: #ffffff;\n        padding: 0px 15px 0px 15px;\n    }\n\n        #header .title img {\n            vertical-align: text-bottom;\n        }\n\n        #header .title .text {\n            font-size: 24px;\n            font-weight: 600;\n            font-family: Arial;\n            margin-left: 4px;\n        }\n\n    #header .menu {\n        float: right;\n        padding: 6px;\n    }\n\n        #header .menu .menu-title {\n            color: #ffffff;\n            font-family: Arial;\n            font-size: 16px;\n        }\n\n#content {\n    min-height: 100%;\n}\n\n.container {\n    margin-left: auto;\n    margin-right: auto;\n    padding: 55px 15px 60px 15px;\n    word-wrap: break-word;\n    min-width: 970px;\n}\n\n    .container .pageLogin {\n        display: none;\n        margin: auto;\n        width: 500px;\n        padding: 150px 0 0 0;\n    }\n\n    .container .page {\n        display: none;\n    }\n\n.auto-resize-img {\n    max-width: 100%;\n    height: auto;\n    display: block;\n    margin-right: auto;\n    margin-left: auto;\n}\n\n.center-iframe {\n    display: block;\n    margin-right: auto;\n    margin-left: auto;\n    max-width: 640px;\n    max-height: 480px;\n}\n\n    .center-iframe iframe {\n        width: 100%;\n        height: 100%;\n    }\n\n#footer {\n    background-color: rgb(243, 243, 243);\n    padding: 20px 0px 20px 0px;\n    margin-top: -55px;\n    box-shadow: 0px 2px 15px 1px #888888;\n    clear: both;\n    position: relative;\n    height: 55px;\n    min-width: 970px;\n}\n\n    #footer .content {\n        margin: 0 auto;\n        color: rgb(119,119,119);\n        font-family: Arial, sans-serif;\n        font-size: 11px;\n        font-weight: 600;\n        text-align: center;\n    }\n\n        #footer .content a {\n            color: #6699ff;\n            text-decoration: none;\n        }\n\n            #footer .content a:hover {\n                color: #6699ff;\n            }\n\n@media (min-width: 992px) {\n    #header .title, .container, #footer .content {\n        width: 970px;\n    }\n}\n\n@media (min-width: 1200px) {\n    #header .title, .container, #footer .content {\n        width: 1170px;\n    }\n}\n\n.form-inline .form-group {\n    margin-right: 10px;\n    margin-bottom: 10px;\n}\n\n.AlertPlaceholder {\n    position: fixed;\n    width: 800px;\n    margin: auto;\n    left: 0;\n    right: 0;\n    top: 45px;\n    z-index: 1000;\n}\n\n.zone-list-pane {\n    float: left;\n    width: 24%;\n}\n\n.zones {\n    font-size: 14px;\n}\n\n    .zones .zone {\n        padding: 4px;\n    }\n\n.zone-viewer-pane {\n    float: right;\n    width: 75%;\n    display: none;\n}\n\n.log-list-pane {\n    float: left;\n    width: 17%;\n}\n\n.logs {\n    font-size: 12px;\n}\n\n    .logs .log {\n        padding: 2px;\n    }\n\n.log-viewer-pane {\n    float: right;\n    width: 82%;\n    display: none;\n}\n\n.query-logs tr:hover {\n    backdrop-filter: brightness(95%);\n}\n\n.stats-panel {\n    height: 80px;\n    padding: 6px 0 6px 0;\n}\n\n    .stats-panel .total-queries {\n        background-color: rgba(102, 153, 255, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .no-error {\n        background-color: rgba(92, 184, 92, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .server-failure {\n        background-color: rgba(217, 83, 79, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .nxdomain {\n        background-color: rgba(120, 120, 120, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .refused {\n        background-color: rgba(91, 192, 222, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .auth-hit {\n        background-color: rgba(150, 150, 0, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .cache-hit {\n        background-color: rgba(111, 84, 153, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .blocked {\n        background-color: rgba(255, 165, 0, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .dropped {\n        background-color: rgba(30, 30, 30, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .recursions {\n        background-color: rgba(23, 162, 184, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .clients {\n        background-color: rgba(51, 122, 183, 0.7);\n        color: #ffffff;\n    }\n\n    .stats-panel .stats-last-item {\n        margin-right: 0% !important;\n    }\n\n    .stats-panel .stats-item {\n        width: 8.818%;\n        float: left;\n        padding: 4px;\n        margin-right: 0.3%;\n    }\n\n        .stats-panel .stats-item .number {\n            font-size: 15px;\n            font-weight: bold;\n        }\n\n        .stats-panel .stats-item .percentage {\n            font-size: 10px;\n            font-weight: bold;\n        }\n\n        .stats-panel .stats-item .title {\n            font-size: 12px;\n            font-weight: bold;\n        }\n\n.zone-stats-panel {\n    margin-bottom: 15px;\n}\n\n    .zone-stats-panel .stats-last-item {\n        margin-right: 0% !important;\n    }\n\n    .zone-stats-panel .stats-item {\n        width: 16.125%;\n        float: left;\n        padding: 6px 4px;\n        margin-right: 0.65%;\n        background-color: rgba(51, 122, 183, 0.7);\n        color: #ffffff;\n    }\n\n        .zone-stats-panel .stats-item .number {\n            font-size: 14px;\n            font-weight: bold;\n            padding: 6px 0;\n        }\n\n        .zone-stats-panel .stats-item .title {\n            font-size: 12px;\n            font-weight: bold;\n        }\n\n.about p {\n    color: rgb(119, 119, 119);\n    text-align: center;\n}\n\n.about h3 a {\n    color: rgb(51,51,51) !important;\n}\n\n.cluster-node-dropdown {\n    margin-left: 4px;\n    padding: 2px 8px;\n    height: 28px;\n    max-width: 250px;\n}\n\n.dark-mode {\n    scrollbar-width: thin;\n    scrollbar-color: #555 #2c2c2e;\n}\n\n    .dark-mode ::-webkit-scrollbar {\n        width: 12px;\n        height: 12px;\n    }\n\n    .dark-mode ::-webkit-scrollbar-track {\n        background: #2c2c2e;\n    }\n\n    .dark-mode ::-webkit-scrollbar-thumb {\n        background-color: #555;\n        border-radius: 6px;\n        border: 3px solid #2c2c2e;\n    }\n\nbody.dark-mode {\n    background-color: #1a1a1a !important;\n    color: #dcdcdc !important;\n}\n\n.dark-mode th a,\n.dark-mode th a:hover {\n    color: #dcdcdc !important;\n    text-decoration: none !important;\n}\n\n.dark-mode #header {\n    background-color: #2c2c2e !important;\n    box-shadow: 0px 1px 15px 0px #000 !important;\n}\n\n.dark-mode #footer {\n    background-color: #252525 !important;\n    color: #888888 !important;\n    box-shadow: 0px 2px 15px 1px #000 !important;\n}\n\n    .dark-mode #footer .content {\n        color: #888888 !important;\n    }\n\n.dark-mode .about h1 {\n    color: #f0f0f0 !important;\n}\n\n.dark-mode .about p {\n    color: #a0a0a0 !important;\n}\n\n.dark-mode .about h3 a {\n    color: #f0f0f0 !important;\n}\n\n.dark-mode .panel,\n.dark-mode .panel-default {\n    background-color: #2c2c2e !important;\n    border-color: #3a3a3c !important;\n}\n\n.dark-mode .panel-heading {\n    background-color: #3a3a3c !important;\n    border-color: #4a4a4c !important;\n    color: #f5f5f7 !important;\n}\n\n.dark-mode .panel-body {\n    background-color: #252525 !important;\n    color: #dcdcdc !important;\n}\n\n.dark-mode .navbar-default {\n    background-color: #2c2c2e !important;\n    border-color: #3a3a3c !important;\n}\n\n.dark-mode .dropdown-menu {\n    background-color: #2c2c2e !important;\n    border-color: #3a3a3c !important;\n}\n\n    .dark-mode .dropdown-menu > li > a:hover,\n    .dark-mode .dropdown-menu > li > a:focus {\n        background-color: #3a3a3c !important;\n    }\n\n    .dark-mode .dropdown-menu li a {\n        color: #ffffff !important;\n    }\n\n.dark-mode .divider {\n    background-color: #3a3a3c !important;\n}\n\n.dark-mode .nav-tabs {\n    border-bottom: 1px solid #007aff !important;\n}\n\n    .dark-mode .nav-tabs > li > a:hover {\n        border-color: #4a4a4c !important;\n        border-bottom-color: #007aff !important;\n        background-color: #3a3a3c;\n    }\n\n    .dark-mode .nav-tabs > li.active > a,\n    .dark-mode .nav-tabs > li.active > a:hover,\n    .dark-mode .nav-tabs > li.active > a:focus {\n        background-color: #252525;\n        border-color: #007aff !important;\n        color: #ffffff !important;\n        border-bottom-color: transparent !important;\n    }\n\n.dark-mode .table-hover > tbody > tr:hover {\n    background-color: #33373a !important;\n}\n\n.dark-mode .table-striped > tbody > tr:nth-of-type(odd) {\n    background-color: #28282a !important;\n}\n\n.dark-mode .table > thead > tr > th,\n.dark-mode .table > tbody > tr > td,\n.dark-mode .table > tfoot > tr > th,\n.dark-mode .table > tfoot > tr > td {\n    border-top-color: #3a3a3c !important;\n}\n\n.dark-mode .table-bordered,\n.dark-mode .table-bordered > thead > tr > th,\n.dark-mode .table-bordered > tbody > tr > td {\n    border-color: #3a3a3c !important;\n}\n\n.dark-mode .form-control {\n    background-color: #3a3a3c !important;\n    border-color: #4a4a4c !important;\n    color: #f5f5f7 !important;\n}\n\n    .dark-mode .form-control[disabled], .dark-mode .form-control[readonly] {\n        background-color: #2c2c2e !important;\n    }\n\n.dark-mode .modal-content {\n    background-color: #2c2c2e !important;\n    border-color: #3a3a3c !important;\n}\n\n.dark-mode .modal-header {\n    border-bottom-color: #3a3a3c !important;\n}\n\n.dark-mode .modal-footer {\n    border-top-color: #3a3a3c !important;\n}\n\n.dark-mode .well {\n    background-color: #252525 !important;\n    border-color: #3a3a3c !important;\n}\n\n.dark-mode pre {\n    background-color: #222 !important;\n    color: #dcdcdc !important;\n    border: 1px solid #4a4a4c !important;\n}\n\n.dark-mode .btn-default {\n    color: #f5f5f7 !important;\n    background-color: #3a3a3c !important;\n    border-color: #4a4a4c !important;\n}\n\n    .dark-mode .btn-default:hover,\n    .dark-mode .btn-default.active {\n        background-color: #4a4a4c !important;\n        border-color: #5a5a5c !important;\n    }\n\n.dark-mode .input-group-addon {\n    background-color: #3a3a3c !important;\n    border-color: #4a4a4c !important;\n}\n\n.dark-mode .stats-panel .stats-item,\n.dark-mode .zone-stats-panel .stats-item {\n    color: #fff !important;\n}\n\n.dark-mode .c3-axis-x text,\n.dark-mode .c3-axis-y text,\n.dark-mode .c3-legend-item {\n    fill: #dcdcdc !important;\n}\n\n.dark-mode .c3-grid line {\n    stroke: #4a4a4c !important;\n}\n\n.dark-mode input[type=\"datetime-local\"] {\n    color-scheme: dark;\n}\n\n    .dark-mode input[type=\"datetime-local\"]::-webkit-calendar-picker-indicator {\n        filter: invert(1);\n    }\n\n.dark-mode .pager li > a {\n    background-color: #3a3a3c !important;\n    border-color: #4a4a4c !important;\n}\n\n    .dark-mode .pager li > a:hover {\n        background-color: #4a4a4c !important;\n    }\n\n.dark-mode .pagination li:not(.active) a {\n    color: white;\n    background-color: #252525;\n    border: 1px solid #337ab7;\n}\n\n    .dark-mode .pagination li:not(.active) a:hover {\n        background-color: #33373a;\n    }\n\n.dark-mode #dpCustomDayWiseStart,\n.dark-mode #dpCustomDayWiseEnd,\n.dark-mode #txtQueryLogStart,\n.dark-mode #txtQueryLogEnd {\n    color-scheme: dark;\n}\n\n    .dark-mode #dpCustomDayWiseStart::-webkit-calendar-picker-indicator,\n    .dark-mode #dpCustomDayWiseEnd::-webkit-calendar-picker-indicator,\n    .dark-mode #txtQueryLogStart::-webkit-calendar-picker-indicator,\n    .dark-mode #txtQueryLogEnd::-webkit-calendar-picker-indicator {\n        filter: invert(1);\n    }\n"
  },
  {
    "path": "DnsServerCore/www/index.html",
    "content": "﻿<!DOCTYPE html>\n\n<!--\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n-->\n\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"robots\" content=\"noindex, nofollow\" />\n    <meta name=\"referrer\" content=\"no-referrer\" />\n\n    <title>Technitium DNS Server</title>\n\n    <script src=\"js/jquery.min.js\"></script>\n\n    <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\">\n    <script src=\"js/bootstrap.min.js\"></script>\n    <script src=\"js/Chart.min.js\"></script>\n\n    <link href=\"css/font-awesome.min.css\" rel=\"stylesheet\" />\n\n    <script src=\"js/moment.min.js\"></script>\n\n    <link href=\"css/main.css\" rel=\"stylesheet\" />\n    <script src=\"js/common.js\"></script>\n    <script src=\"js/main.js\"></script>\n    <script src=\"js/auth.js\"></script>\n    <script src=\"js/cluster.js\"></script>\n    <script src=\"js/zone.js\"></script>\n    <script src=\"js/other-zones.js\"></script>\n    <script src=\"js/apps.js\"></script>\n    <script src=\"js/dnsclient.js\"></script>\n    <script src=\"js/dhcp.js\"></script>\n    <script src=\"js/logs.js\"></script>\n</head>\n<body style=\"background-color: #fafafa;\">\n    <div id=\"header\">\n        <div id=\"mnuUser\" class=\"menu dropdown\" style=\"display: none;\">\n            <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\" role=\"button\" aria-haspopup=\"true\" aria-expanded=\"false\" style=\"text-decoration: none;\">\n                <span class=\"menu-title\">\n                    <span class=\"glyphicon glyphicon-user\" aria-hidden=\"true\"></span>\n                    <span id=\"mnuUserDisplayName\"></span>\n                    <span class=\"caret\"></span>\n                </span>\n            </a>\n            <ul class=\"dropdown-menu\">\n                <li><a href=\"#\" onclick=\"showMyProfileModal(); return false;\">My Profile</a></li>\n                <li><a href=\"#\" onclick=\"showCreateMyApiTokenModal(); return false;\">Create API Token</a></li>\n                <li><a href=\"#\" onclick=\"showChangePasswordModal(); return false;\">Change Password</a></li>\n                <li><a href=\"#\" onclick=\"showConfigure2FAModal(); return false;\">Configure 2FA</a></li>\n                <li><a href=\"#\" onclick=\"toggleTheme(); return false;\">Toggle Dark Mode</a></li>\n                <li role=\"separator\" class=\"divider\"></li>\n                <li><a href=\"#\" onclick=\"logout(); return false;\">Logout</a></li>\n            </ul>\n        </div>\n    </div>\n\n    <div id=\"content\">\n        <div class=\"container\">\n            <div class=\"AlertPlaceholder\"></div>\n\n            <div id=\"pageLogin\" class=\"pageLogin\">\n                <div class=\"panel panel-default\">\n                    <div class=\"panel-heading\">\n                        <h3 class=\"panel-title\">DNS Server</h3>\n                    </div>\n                    <div class=\"panel-body\">\n                        <form class=\"form-horizontal\">\n                            <div class=\"form-group\">\n                                <label for=\"txtUser\" class=\"col-sm-3 control-label\">Username</label>\n                                <div class=\"col-sm-8\">\n                                    <input type=\"text\" class=\"form-control\" id=\"txtUser\" placeholder=\"username\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtPass\" class=\"col-sm-3 control-label\">Password</label>\n                                <div class=\"col-sm-8\">\n                                    <input type=\"password\" class=\"form-control\" id=\"txtPass\" placeholder=\"password\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" id=\"div2FAOTP\">\n                                <label for=\"txt2FATOTP\" class=\"col-sm-3 control-label\">Enter OTP</label>\n                                <div class=\"col-sm-8\">\n                                    <input id=\"txt2FATOTP\" type=\"text\" class=\"form-control\" style=\"width: 100px;\" placeholder=\"OTP\" maxlength=\"6\">\n                                    <div style=\"padding-top: 5px;\">Enter the 6-digit code you see in your authenticator app.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                <div class=\"col-sm-offset-3 col-sm-4\">\n                                    <button id=\"btnLogin\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Working...\" onclick=\"login(); return false;\">Login</button>\n                                </div>\n                                <div class=\"col-sm-4\" style=\"padding: 6px; text-align: right;\">\n                                    <a href=\"#\" data-toggle=\"modal\" data-target=\"#modalForgotPassword\">Forgot Password?</a>\n                                </div>\n                            </div>\n                        </form>\n                    </div>\n                </div>\n            </div>\n\n            <div id=\"pageMain\" class=\"page\">\n                <div class=\"panel panel-default\">\n                    <div class=\"panel-heading\" style=\"height: 38px;\">\n                        <div style=\"float: left;\">\n                            <h3 class=\"panel-title\">DNS Server<span id=\"lblDnsServerDomain\"></span></h3>\n                        </div>\n                        <div style=\"float: right;\">\n                            <a href=\"#\" data-toggle=\"modal\" data-target=\"#modalUpdateAvailable\" id=\"lnkUpdateAvailable\" style=\"display: none; color: red !important;\">New Update Available!</a>\n                        </div>\n                    </div>\n\n                    <div class=\"panel-body\" style=\"min-height: 500px;\">\n                        <div>\n                            <ul class=\"nav nav-tabs\" role=\"tablist\">\n                                <li id=\"mainPanelTabListDashboard\" role=\"presentation\" class=\"active\"><a href=\"#mainPanelTabPaneDashboard\" aria-controls=\"mainPanelTabPaneDashboard\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshDashboard();\">Dashboard</a></li>\n                                <li id=\"mainPanelTabListZones\" role=\"presentation\"><a href=\"#mainPanelTabPaneZones\" aria-controls=\"mainPanelTabPaneZones\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshZones(true);\">Zones</a></li>\n                                <li id=\"mainPanelTabListCachedZones\" role=\"presentation\"><a href=\"#mainPanelTabPaneCachedZones\" aria-controls=\"mainPanelTabPaneCachedZones\" role=\"tab\" data-toggle=\"tab\">Cache</a></li>\n                                <li id=\"mainPanelTabListAllowedZones\" role=\"presentation\"><a href=\"#mainPanelTabPaneAllowedZones\" aria-controls=\"mainPanelTabPaneAllowedZones\" role=\"tab\" data-toggle=\"tab\">Allowed</a></li>\n                                <li id=\"mainPanelTabListBlockedZones\" role=\"presentation\"><a href=\"#mainPanelTabPaneBlockedZones\" aria-controls=\"mainPanelTabPaneBlockedZones\" role=\"tab\" data-toggle=\"tab\">Blocked</a></li>\n                                <li id=\"mainPanelTabListApps\" role=\"presentation\"><a href=\"#mainPanelTabPaneApps\" aria-controls=\"mainPanelTabPaneApps\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshApps();\">Apps</a></li>\n                                <li id=\"mainPanelTabListDnsClient\" role=\"presentation\"><a href=\"#mainPanelTabPaneDnsClient\" aria-controls=\"mainPanelTabPaneDnsClient\" role=\"tab\" data-toggle=\"tab\">DNS Client</a></li>\n                                <li id=\"mainPanelTabListSettings\" role=\"presentation\"><a href=\"#mainPanelTabPaneSettings\" aria-controls=\"mainPanelTabPaneSettings\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshDnsSettings();\">Settings</a></li>\n                                <li id=\"mainPanelTabListDhcp\" role=\"presentation\"><a href=\"#mainPanelTabPaneDhcp\" aria-controls=\"mainPanelTabPaneDhcp\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshDhcpTab();\">DHCP</a></li>\n                                <li id=\"mainPanelTabListAdmin\" role=\"presentation\"><a href=\"#mainPanelTabPaneAdmin\" aria-controls=\"mainPanelTabPaneAdmin\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshAdminTab();\">Administration</a></li>\n                                <li id=\"mainPanelTabListLogs\" role=\"presentation\"><a href=\"#mainPanelTabPaneLogs\" aria-controls=\"mainPanelTabPaneLogs\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshLogsTab();\">Logs</a></li>\n                                <li id=\"mainPanelTabListAbout\" role=\"presentation\"><a href=\"#mainPanelTabPaneAbout\" aria-controls=\"mainPanelTabPaneAbout\" role=\"tab\" data-toggle=\"tab\">About</a></li>\n                            </ul>\n\n                            <div class=\"tab-content\">\n                                <div id=\"mainPanelTabPaneDashboard\" role=\"tabpanel\" class=\"tab-pane active\" style=\"padding: 10px 0 0 0;\">\n                                    <div>\n                                        <div class=\"pull-left\">\n                                            <div class=\"btn-group\" data-toggle=\"buttons\">\n                                                <label class=\"btn btn-default active\">\n                                                    <input type=\"radio\" name=\"rdStatType\" value=\"lastHour\" autocomplete=\"off\" checked> Last Hour\n                                                </label>\n                                                <label class=\"btn btn-default\">\n                                                    <input type=\"radio\" name=\"rdStatType\" value=\"lastDay\" autocomplete=\"off\"> Last Day\n                                                </label>\n                                                <label class=\"btn btn-default\">\n                                                    <input type=\"radio\" name=\"rdStatType\" value=\"lastWeek\" autocomplete=\"off\"> Last Week\n                                                </label>\n                                                <label class=\"btn btn-default\">\n                                                    <input type=\"radio\" name=\"rdStatType\" value=\"lastMonth\" autocomplete=\"off\"> Last Month\n                                                </label>\n                                                <label class=\"btn btn-default\">\n                                                    <input type=\"radio\" name=\"rdStatType\" value=\"lastYear\" autocomplete=\"off\"> Last Year\n                                                </label>\n                                                <label class=\"btn btn-default\">\n                                                    <input type=\"radio\" name=\"rdStatType\" value=\"custom\" autocomplete=\"off\"> Custom\n                                                </label>\n                                            </div>\n\n                                            <div id=\"divCustomDayWise\" style=\"padding: 6px 0px 0px; display: none;\">\n                                                <span style=\"margin-right: 6px;\"><label for=\"dpCustomDayWiseStart\">Start</label> <input type=\"datetime-local\" id=\"dpCustomDayWiseStart\" size=\"10\"></span>\n                                                <span style=\"margin-right: 6px;\"><label for=\"dpCustomDayWiseEnd\">End</label> <input type=\"datetime-local\" id=\"dpCustomDayWiseEnd\" size=\"10\"></span>\n                                                <button id=\"btnCustomDayWise\" class=\"btn btn-default\" type=\"button\" style=\"font-size: 12px; padding: 3px 0px; width: 60px; vertical-align: top;\">Show</button>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"pull-right\">\n                                            <select id=\"optDashboardClusterNode\" class=\"form-control cluster-node-dropdown\" style=\"margin-left: 0px;\" onchange=\"refreshDashboard();\"></select>\n                                        </div>\n\n                                        <div class=\"clearfix\"></div>\n                                    </div>\n\n                                    <div id=\"divDashboardLoader\" style=\"margin-top: 10px; height: 400px;\"></div>\n\n                                    <div id=\"divDashboard\" style=\"display: none;\">\n                                        <div class=\"stats-panel\">\n                                            <div class=\"stats-item total-queries\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalQueries\">100</div>\n                                                <div class=\"percentage\">100%</div>\n                                                <div class=\"title\">Total Queries</div>\n                                            </div>\n\n                                            <div class=\"stats-item no-error\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalNoError\">70</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalNoErrorPercentage\">0%</div>\n                                                <div class=\"title\">No Error</div>\n                                            </div>\n\n                                            <div class=\"stats-item server-failure\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalServerFailure\">5</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalServerFailurePercentage\">0%</div>\n                                                <div class=\"title\">Server Failure</div>\n                                            </div>\n\n                                            <div class=\"stats-item nxdomain\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalNxDomain\">5</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalNxDomainPercentage\">0%</div>\n                                                <div class=\"title\">NX Domain</div>\n                                            </div>\n\n                                            <div class=\"stats-item refused\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalRefused\">10</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalRefusedPercentage\">0%</div>\n                                                <div class=\"title\">Refused</div>\n                                            </div>\n\n                                            <div class=\"stats-item auth-hit\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalAuthHit\">10</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalAuthHitPercentage\">0%</div>\n                                                <div class=\"title\">Authoritative</div>\n                                            </div>\n\n                                            <div class=\"stats-item recursions\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalRecursions\">10</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalRecursionsPercentage\">0%</div>\n                                                <div class=\"title\">Recursive</div>\n                                            </div>\n\n                                            <div class=\"stats-item cache-hit\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalCacheHit\">10</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalCacheHitPercentage\">0%</div>\n                                                <div class=\"title\">Cached</div>\n                                            </div>\n\n                                            <div class=\"stats-item blocked\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalBlocked\">10</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalBlockedPercentage\">0%</div>\n                                                <div class=\"title\">Blocked</div>\n                                            </div>\n\n                                            <div class=\"stats-item dropped\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalDropped\">5</div>\n                                                <div class=\"percentage\" id=\"divDashboardStatsTotalDroppedPercentage\">0%</div>\n                                                <div class=\"title\">Dropped</div>\n                                            </div>\n\n                                            <div class=\"stats-item stats-last-item clients\">\n                                                <div class=\"number\" id=\"divDashboardStatsTotalClients\">10</div>\n                                                <div class=\"percentage\">&nbsp;</div>\n                                                <div class=\"title\">Clients</div>\n                                            </div>\n                                        </div>\n\n                                        <div>\n                                            <canvas id=\"canvasDashboardMain\" style=\"margin: 10px 0 10px 0;\"></canvas>\n                                        </div>\n\n                                        <div style=\"margin-top: 15px;\">\n                                            <div style=\"float: left; width: 50%; padding-right: 7px;\">\n                                                <div class=\"panel panel-default\" style=\"margin-bottom: 15px;\">\n                                                    <div class=\"panel-body\">\n                                                        <canvas id=\"canvasDashboardPie\"></canvas>\n                                                    </div>\n                                                </div>\n\n                                                <div class=\"panel panel-default\" style=\"margin-bottom: 15px;\">\n                                                    <div class=\"panel-body\">\n                                                        <canvas id=\"canvasDashboardPie2\"></canvas>\n                                                    </div>\n                                                </div>\n\n                                                <div class=\"panel panel-default\" style=\"margin-bottom: 0px;\">\n                                                    <div class=\"panel-body\">\n                                                        <canvas id=\"canvasDashboardPie3\"></canvas>\n                                                    </div>\n                                                </div>\n                                            </div>\n\n                                            <div style=\"float: right; width: 50%; padding-left: 7px;\">\n                                                <div class=\"zone-stats-panel\">\n                                                    <div class=\"stats-item\">\n                                                        <div class=\"number\" id=\"divDashboardStatsZones\">10</div>\n                                                        <div class=\"title\">Zones</div>\n                                                    </div>\n\n                                                    <div class=\"stats-item\">\n                                                        <div class=\"number\" id=\"divDashboardStatsCachedEntries\">10</div>\n                                                        <div class=\"title\">Cache</div>\n                                                    </div>\n\n                                                    <div class=\"stats-item\">\n                                                        <div class=\"number\" id=\"divDashboardStatsAllowedZones\">10</div>\n                                                        <div class=\"title\">Allowed</div>\n                                                    </div>\n\n                                                    <div class=\"stats-item\">\n                                                        <div class=\"number\" id=\"divDashboardStatsBlockedZones\">10</div>\n                                                        <div class=\"title\">Blocked</div>\n                                                    </div>\n\n                                                    <div class=\"stats-item\">\n                                                        <div class=\"number\" id=\"divDashboardStatsAllowListZones\">10</div>\n                                                        <div class=\"title\">Allow List</div>\n                                                    </div>\n\n                                                    <div class=\"stats-item stats-last-item\">\n                                                        <div class=\"number\" id=\"divDashboardStatsBlockListZones\">10</div>\n                                                        <div class=\"title\">Block List</div>\n                                                    </div>\n\n                                                    <div class=\"clearfix\"></div>\n                                                </div>\n\n                                                <div class=\"panel panel-default\" style=\"margin-bottom: 0px;\">\n                                                    <div class=\"panel-heading\" style=\"height: 41px; padding: 4px 6px;\">\n                                                        <div class=\"pull-left\" style=\"padding: 6px 8px;\">Top Clients</div>\n                                                        <div class=\"pull-right\">\n                                                            <button type=\"button\" class=\"btn btn-default\" data-loading-text=\"More\" onclick=\"showTopStats('TopClients', 1000);\" style=\"font-size: 12px; padding: 4px 14px; margin: 2px 0px;\">More</button>\n                                                        </div>\n                                                        <div class=\"clearfix\"></div>\n                                                    </div>\n                                                    <table class=\"table table-hover\">\n                                                        <thead>\n                                                            <tr>\n                                                                <th>Client</th>\n                                                                <th>Queries</th>\n                                                                <th style=\"width: 36px;\"></th>\n                                                            </tr>\n                                                        </thead>\n                                                        <tbody id=\"tableTopClients\">\n                                                        </tbody>\n                                                    </table>\n                                                </div>\n                                            </div>\n\n                                            <div style=\"clear: both;\"></div>\n                                        </div>\n\n                                        <div style=\"margin-top: 15px;\">\n                                            <div style=\"float: left; width: 50%; padding-right: 7px;\">\n                                                <div class=\"panel panel-default\" style=\"margin-bottom: 0px;\">\n                                                    <div class=\"panel-heading\" style=\"height: 41px; padding: 4px 6px;\">\n                                                        <div class=\"pull-left\" style=\"padding: 6px 8px;\">Top Domains</div>\n                                                        <div class=\"pull-right\">\n                                                            <button type=\"button\" class=\"btn btn-default\" data-loading-text=\"More\" onclick=\"showTopStats('TopDomains', 1000);\" style=\"font-size: 12px; padding: 4px 14px; margin: 2px 0px;\">More</button>\n                                                        </div>\n                                                        <div class=\"clearfix\"></div>\n                                                    </div>\n                                                    <table class=\"table table-hover\">\n                                                        <thead>\n                                                            <tr>\n                                                                <th>Domain</th>\n                                                                <th>Hits</th>\n                                                                <th style=\"width: 36px;\"></th>\n                                                            </tr>\n                                                        </thead>\n                                                        <tbody id=\"tableTopDomains\">\n                                                        </tbody>\n                                                    </table>\n                                                </div>\n                                            </div>\n\n                                            <div style=\"float: right; width: 50%; padding-left: 7px;\">\n                                                <div class=\"panel panel-default\" style=\"margin-bottom: 0px;\">\n                                                    <div class=\"panel-heading\" style=\"height: 41px; padding: 4px 6px;\">\n                                                        <div class=\"pull-left\" style=\"padding: 6px 8px;\">Top Blocked Domains</div>\n                                                        <div class=\"pull-right\">\n                                                            <button type=\"button\" class=\"btn btn-default\" data-loading-text=\"More\" onclick=\"showTopStats('TopBlockedDomains', 1000);\" style=\"font-size: 12px; padding: 4px 14px; margin: 2px 0px;\">More</button>\n                                                        </div>\n                                                        <div class=\"clearfix\"></div>\n                                                    </div>\n                                                    <table class=\"table table-hover\">\n                                                        <thead>\n                                                            <tr>\n                                                                <th>Domain</th>\n                                                                <th>Hits</th>\n                                                                <th style=\"width: 36px;\"></th>\n                                                            </tr>\n                                                        </thead>\n                                                        <tbody id=\"tableTopBlockedDomains\">\n                                                        </tbody>\n                                                    </table>\n                                                </div>\n                                            </div>\n\n                                            <div style=\"clear: both;\"></div>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div id=\"mainPanelTabPaneZones\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n                                    <div id=\"divViewZonesLoader\" style=\"display: none; margin-top: 10px; height: 400px;\"></div>\n\n                                    <div id=\"divViewZones\">\n                                        <div class=\"form-inline\" style=\"margin-bottom: 4px;\">\n                                            <div class=\"pull-right\">\n                                                <div class=\"form-group\" style=\"margin-right: 0px;\">\n                                                    <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showAddZoneModal();\">Add Zone</button>\n                                                    <select id=\"optZonesClusterNode\" class=\"form-control cluster-node-dropdown\" onchange=\"refreshZones();\"></select>\n                                                </div>\n                                            </div>\n                                            <div class=\"clearfix\"></div>\n                                        </div>\n\n                                        <div class=\"form-inline\">\n                                            <form class=\"pull-left\">\n                                                <div class=\"form-group\">\n                                                    <input id=\"txtZonesEdit\" class=\"form-control\" style=\"width: 300px;\" type=\"text\" placeholder=\"example.com\" />\n                                                    <button type=\"submit\" class=\"btn btn-primary\" onclick=\"showEditZone(); return false;\">Edit</button>\n                                                </div>\n                                            </form>\n                                            <form class=\"pull-right\">\n                                                <div class=\"form-group\">\n                                                    <label for=\"txtZonesPageNumber\">Page Number</label>\n                                                    <input id=\"txtZonesPageNumber\" class=\"form-control\" style=\"width: 100px;\" type=\"number\" />\n                                                </div>\n                                                <div class=\"form-group\">\n                                                    <label for=\"optZonesPerPage\">Zones Per Page</label>\n                                                    <select class=\"form-control\" id=\"optZonesPerPage\">\n                                                        <option selected>10</option>\n                                                        <option>25</option>\n                                                        <option>50</option>\n                                                        <option>100</option>\n                                                        <option>250</option>\n                                                        <option>500</option>\n                                                    </select>\n                                                </div>\n                                                <button type=\"submit\" class=\"btn btn-primary form-group\" style=\"margin-right: 0px;\" onclick=\"refreshZones(); return false;\">Go</button>\n                                            </form>\n                                            <div class=\"clearfix\"></div>\n                                        </div>\n\n                                        <div style=\"padding: 8px;\">\n                                            <div class=\"pull-left\" style=\"padding-top: 8px;\">\n                                                <b id=\"tableZonesTopStatus\">0 zones</b>\n                                            </div>\n                                            <div class=\"pull-right\">\n                                                <nav aria-label=\"Page navigation\">\n                                                    <ul id=\"tableZonesTopPagination\" class=\"pagination\" style=\"margin: 0;\">\n                                                        <li><a href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">&laquo;</span></a></li>\n                                                        <li><a href=\"#\">1</a></li>\n                                                        <li><a href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">&raquo;</span></a></li>\n                                                    </ul>\n                                                </nav>\n                                            </div>\n                                            <div class=\"clearfix\"></div>\n                                        </div>\n\n                                        <table id=\"tableZones\" class=\"table table-hover\">\n                                            <thead>\n                                                <tr>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 0); return false;\">#</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 1); return false;\">Zone</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 2); return false;\">Type</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 3); return false;\">DNSSEC</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 4); return false;\">Status</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 5); return false;\">Serial</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 6); return false;\">Expiry</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableZonesBody', 7); return false;\">Last Modified</a></th>\n                                                    <th style=\"width: 36px;\"></th>\n                                                </tr>\n                                            </thead>\n                                            <tbody id=\"tableZonesBody\"></tbody>\n                                            <tfoot>\n                                                <tr>\n                                                    <td colspan=\"9\">\n                                                        <div>\n                                                            <div class=\"pull-left\" style=\"padding-top: 8px;\">\n                                                                <b id=\"tableZonesFooterStatus\">0 zones</b>\n                                                            </div>\n                                                            <div class=\"pull-right\">\n                                                                <nav aria-label=\"Page navigation\">\n                                                                    <ul id=\"tableZonesFooterPagination\" class=\"pagination\" style=\"margin: 0;\">\n                                                                        <li><a href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">&laquo;</span></a></li>\n                                                                        <li><a href=\"#\">1</a></li>\n                                                                        <li><a href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">&raquo;</span></a></li>\n                                                                    </ul>\n                                                                </nav>\n                                                            </div>\n                                                            <div class=\"clearfix\"></div>\n                                                        </div>\n                                                    </td>\n                                                </tr>\n                                            </tfoot>\n                                        </table>\n                                    </div>\n\n                                    <div id=\"divEditZone\" style=\"display: none;\">\n                                        <div>\n                                            <div class=\"pull-left\">\n                                                <ul class=\"pager\" style=\"margin: 0px;\">\n                                                    <li class=\"previous\"><a href=\"#\" onclick=\"refreshZones(); return false;\"><span aria-hidden=\"true\">&larr;</span> Back</a></li>\n                                                </ul>\n                                            </div>\n\n                                            <div class=\"pull-right\">\n                                                <select id=\"optEditZoneClusterNode\" class=\"form-control cluster-node-dropdown\" style=\"margin-left: 0px;\" disabled></select>\n                                            </div>\n\n                                            <div class=\"clearfix\"></div>\n                                        </div>\n\n                                        <div style=\"padding: 10px 0px;\">\n                                            <h3 style=\"margin: 4px 0;\"><span id=\"titleEditZone\" style=\"margin-right: 10px;\">example.com</span><a href=\"#\" onclick=\"showEditZone($('#titleEditZone').attr('data-zone'), $('#txtEditZonePageNumber').val(), $('#txtEditZoneFilterName').val(), $('#txtEditZoneFilterType').val()); return false;\"><span class=\"glyphicon glyphicon-refresh\" style=\"font-size: 20px;\" aria-hidden=\"true\"></span></a></h3>\n                                            <div style=\"float: left;\">\n                                                <span id=\"titleEditZoneType\" class=\"label label-default\">Primary</span>\n                                                <span id=\"titleEditZoneDnssecStatus\" class=\"label label-default\">DNSSEC</span>\n                                                <span id=\"titleEditZoneStatus\" class=\"label label-success\">Enabled</span>\n                                                <span id=\"titleEditZoneCatalog\" class=\"label label-default\">catalog</span>\n                                                <div id=\"titleEditZoneExpiry\" style=\"padding-top: 4px; font-size: 10px; font-weight: bold;\">Expiry: 01 Jan 2020 00:00:00</div>\n                                            </div>\n                                            <div style=\"float: right; padding: 2px 0px;\">\n                                                <button id=\"btnEditZoneAddRecord\" type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showAddRecordModal();\">Add Record</button>\n                                                <button id=\"btnEnableZoneEditZone\" type=\"button\" class=\"btn btn-default\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"enableZone(this);\">Enable Zone</button>\n                                                <button id=\"btnDisableZoneEditZone\" type=\"button\" class=\"btn btn-warning\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"disableZone(this);\">Disable Zone</button>\n                                                <button id=\"btnEditZoneDeleteZone\" type=\"button\" class=\"btn btn-danger\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"deleteZone(this);\">Delete Zone</button>\n                                                <button id=\"btnZoneResync\" type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"resyncZone(this, $('#titleEditZone').attr('data-zone'));\" data-loading-text=\"Resyncing...\">Resync</button>\n                                                <div id=\"divOptionsMenu\" class=\"btn-group\">\n                                                    <button type=\"button\" class=\"btn btn-primary dropdown-toggle\" style=\"padding: 2px 0px; width: 100px;\" data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n                                                        Options <span class=\"caret\"></span>\n                                                    </button>\n                                                    <ul class=\"dropdown-menu\">\n                                                        <li id=\"lnkImportZone\"><a href=\"#\" onclick=\"showImportZoneModal($('#titleEditZone').attr('data-zone')); return false;\">Import Zone</a></li>\n                                                        <li id=\"lnkExportZone\"><a href=\"#\" onclick=\"exportZone($('#titleEditZone').attr('data-zone')); return false;\">Export Zone</a></li>\n                                                        <li id=\"lnkZoneConvert\"><a href=\"#\" onclick=\"showConvertZoneModal($('#titleEditZone').attr('data-zone'), $('#titleEditZone').attr('data-zone-type')); return false;\">Convert Zone</a></li>\n                                                        <li id=\"lnkCloneZone\"><a href=\"#\" onclick=\"showCloneZoneModal($('#titleEditZone').attr('data-zone')); return false;\">Clone Zone</a></li>\n                                                        <li id=\"lnkZoneOptions\"><a href=\"#\" onclick=\"$('#btnSaveZoneOptions').attr('data-zones-row-id', null); showZoneOptionsModal($('#titleEditZone').attr('data-zone')); return false;\">Zone Options</a></li>\n                                                    </ul>\n                                                </div>\n                                                <button id=\"btnZonePermissions\" type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showZonePermissionsModal($('#titleEditZone').attr('data-zone'));\">Permissions</button>\n                                                <div id=\"divZoneDnssecOptions\" class=\"btn-group\">\n                                                    <button type=\"button\" class=\"btn btn-primary dropdown-toggle\" style=\"padding: 2px 0px; width: 100px;\" data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n                                                        DNSSEC <span class=\"caret\"></span>\n                                                    </button>\n                                                    <ul class=\"dropdown-menu\">\n                                                        <li id=\"lnkZoneDnssecSignZone\"><a href=\"#\" onclick=\"showSignZoneModal($('#titleEditZone').attr('data-zone')); return false;\">Sign Zone</a></li>\n                                                        <li id=\"lnkZoneDnssecHideRecords\"><a href=\"#\" onclick=\"toggleHideDnssecRecords(true); return false;\">Hide DNSSEC Records</a></li>\n                                                        <li id=\"lnkZoneDnssecShowRecords\"><a href=\"#\" onclick=\"toggleHideDnssecRecords(false); return false;\">Show DNSSEC Records</a></li>\n                                                        <li id=\"lnkZoneDnssecViewDsRecords\"><a href=\"#\" onclick=\"showViewDsModal($('#titleEditZone').attr('data-zone')); return false;\">View DS Info</a></li>\n                                                        <li id=\"lnkZoneDnssecProperties\"><a href=\"#\" onclick=\"showDnssecPropertiesModal($('#titleEditZone').attr('data-zone')); return false;\">Properties</a></li>\n                                                        <li id=\"lnkZoneDnssecUnsignZone\"><a href=\"#\" onclick=\"showUnsignZoneModal($('#titleEditZone').attr('data-zone')); return false;\">Unsign Zone</a></li>\n                                                    </ul>\n                                                </div>\n                                            </div>\n                                            <div style=\"clear: both;\"></div>\n                                        </div>\n\n                                        <div class=\"form-inline well\" style=\"padding: 10px 10px 0 10px;margin-bottom: 10px;\">\n                                            <form>\n                                                <div class=\"pull-left\">\n                                                    <div class=\"form-group\">\n                                                        <label for=\"txtEditZoneFilterName\">Name</label>\n                                                        <input id=\"txtEditZoneFilterName\" class=\"form-control\" style=\"width: 300px;\" type=\"text\" placeholder=\"abc or a* or *b* or a?c\" />\n                                                    </div>\n                                                    <div class=\"form-group\">\n                                                        <label for=\"txtEditZoneFilterType\">Type</label>\n                                                        <input id=\"txtEditZoneFilterType\" class=\"form-control\" style=\"width: 130px;\" type=\"text\" />\n                                                    </div>\n                                                </div>\n\n                                                <div class=\"pull-right\">\n                                                    <div class=\"form-group\">\n                                                        <label for=\"txtEditZonePageNumber\">Page Number</label>\n                                                        <input id=\"txtEditZonePageNumber\" class=\"form-control\" style=\"width: 100px;\" type=\"number\" />\n                                                    </div>\n                                                    <div class=\"form-group\">\n                                                        <label for=\"optEditZoneRecordsPerPage\">Records Per Page</label>\n                                                        <select class=\"form-control\" id=\"optEditZoneRecordsPerPage\">\n                                                            <option selected>10</option>\n                                                            <option>25</option>\n                                                            <option>50</option>\n                                                            <option>100</option>\n                                                            <option>250</option>\n                                                            <option>500</option>\n                                                        </select>\n                                                    </div>\n                                                    <button type=\"submit\" class=\"btn btn-primary form-group\" style=\"margin-right: 0px;\" onclick=\"showEditZonePage(); return false;\">Go</button>\n                                                </div>\n\n                                                <div class=\"clearfix\"></div>\n                                            </form>\n                                        </div>\n\n                                        <div style=\"padding: 8px;\">\n                                            <div class=\"pull-left\" style=\"padding-top: 8px;\">\n                                                <b id=\"tableEditZoneTopStatus\">0 records</b>\n                                            </div>\n                                            <div class=\"pull-right\">\n                                                <nav aria-label=\"Page navigation\">\n                                                    <ul id=\"tableEditZoneTopPagination\" class=\"pagination\" style=\"margin: 0;\">\n                                                        <li><a href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">&laquo;</span></a></li>\n                                                        <li><a href=\"#\">1</a></li>\n                                                        <li><a href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">&raquo;</span></a></li>\n                                                    </ul>\n                                                </nav>\n                                            </div>\n                                            <div class=\"clearfix\"></div>\n                                        </div>\n\n                                        <table id=\"tableEditZone\" class=\"table table-hover\">\n                                            <thead>\n                                                <tr>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableEditZoneBody', 0); return false;\">#</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableEditZoneBody', 1); return false;\">Name</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableEditZoneBody', 2); return false;\">Type</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableEditZoneBody', 3); return false;\">TTL</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tableEditZoneBody', 4); return false;\">Data</a></th>\n                                                    <th></th>\n                                                </tr>\n                                            </thead>\n                                            <tbody id=\"tableEditZoneBody\">\n                                            </tbody>\n                                            <tfoot>\n                                                <tr>\n                                                    <td colspan=\"6\">\n                                                        <div>\n                                                            <div class=\"pull-left\" style=\"padding-top: 8px;\">\n                                                                <b id=\"tableEditZoneFooterStatus\">0 records</b>\n                                                            </div>\n                                                            <div class=\"pull-right\">\n                                                                <nav aria-label=\"Page navigation\">\n                                                                    <ul id=\"tableEditZoneFooterPagination\" class=\"pagination\" style=\"margin: 0;\">\n                                                                        <li><a href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">&laquo;</span></a></li>\n                                                                        <li><a href=\"#\">1</a></li>\n                                                                        <li><a href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">&raquo;</span></a></li>\n                                                                    </ul>\n                                                                </nav>\n                                                            </div>\n                                                            <div class=\"clearfix\"></div>\n                                                        </div>\n                                                    </td>\n                                                </tr>\n                                            </tfoot>\n                                        </table>\n                                    </div>\n                                </div>\n\n                                <div id=\"mainPanelTabPaneCachedZones\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n\n                                    <div class=\"well well-sm zone-list-pane\">\n                                        <form class=\"form-inline\">\n                                            <div class=\"form-group\" style=\"width: 100%\">\n                                                <input type=\"text\" class=\"form-control\" style=\"width: inherit;\" id=\"txtCacheZone\" placeholder=\"example.com\">\n                                            </div>\n                                            <div class=\"form-group\">\n                                                <button id=\"btnBrowseCacheZone\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Browse\" onclick=\"refreshCachedZonesList($('#txtCacheZone').val()); return false;\">Browse</button>\n                                            </div>\n                                        </form>\n\n                                        <div id=\"lstCachedZones\" class=\"zones\">\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divCachedZoneViewer\" class=\"zone-viewer-pane\">\n                                        <div class=\"panel panel-default\">\n                                            <div class=\"panel-heading\" style=\"height: 36px; padding: 4px 6px;\">\n                                                <div id=\"txtCachedZoneViewerTitle\" class=\"pull-left\" style=\"padding: 4px;\">technitium.com</div>\n                                                <div class=\"form-inline pull-right\">\n                                                    <button id=\"btnDeleteCachedZone\" type=\"button\" class=\"btn btn-warning\" data-loading-text=\"Delete\" onclick=\"deleteCachedZone();\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Delete</button>\n                                                    <button type=\"button\" class=\"btn btn-danger\" data-loading-text=\"Delete\" onclick=\"flushDnsCache(this, $('#optCachedZonesClusterNode').val());\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Flush</button>\n                                                    <select id=\"optCachedZonesClusterNode\" class=\"form-control cluster-node-dropdown\" style=\"margin-left: 2px;\" onchange=\"refreshCachedZonesList();\"></select>\n                                                </div>\n                                                <div class=\"clearfix\"></div>\n                                            </div>\n\n                                            <div class=\"panel-body\">\n                                                <pre id=\"preCachedZoneViewerBody\">\n\n                                                </pre>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                </div>\n\n                                <div id=\"mainPanelTabPaneAllowedZones\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n\n                                    <div class=\"well well-sm zone-list-pane\">\n                                        <form class=\"form-inline\">\n                                            <div class=\"form-group\" style=\"width: 100%\">\n                                                <input type=\"text\" class=\"form-control\" style=\"width: inherit;\" id=\"txtAllowZone\" placeholder=\"example.com\">\n                                            </div>\n                                            <div class=\"form-group\">\n                                                <button id=\"btnAllowZone\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Allow\" onclick=\"allowZone(); return false;\">Allow</button>\n                                                <button id=\"btnBrowseAllowZone\" type=\"button\" class=\"btn btn-default\" data-loading-text=\"Browse\" onclick=\"refreshAllowedZonesList($('#txtAllowZone').val());\">Browse</button>\n                                            </div>\n                                        </form>\n\n                                        <div id=\"lstAllowedZones\" class=\"zones\">\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divAllowedZoneViewer\" class=\"zone-viewer-pane\">\n                                        <div class=\"panel panel-default\">\n                                            <div class=\"panel-heading\" style=\"height: 36px; padding: 4px 6px;\">\n                                                <div id=\"txtAllowedZoneViewerTitle\" class=\"pull-left\" style=\"padding: 4px;\">technitium.com</div>\n                                                <div class=\"form-inline pull-right\">\n                                                    <button type=\"button\" class=\"btn btn-default\" data-loading-text=\"Import\" onclick=\"resetImportAllowedZonesModal();\" data-toggle=\"modal\" data-target=\"#modalImportAllowedZones\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Import</button>\n                                                    <button type=\"button\" class=\"btn btn-default\" data-loading-text=\"Export\" onclick=\"exportAllowedZones();\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Export</button>\n                                                    <button id=\"btnDeleteAllowedZone\" type=\"button\" class=\"btn btn-warning\" data-loading-text=\"Delete\" onclick=\"deleteAllowedZone();\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Delete</button>\n                                                    <button id=\"btnFlushAllowedZone\" type=\"button\" class=\"btn btn-danger\" data-loading-text=\"Flush\" onclick=\"flushAllowedZone();\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Flush</button>\n                                                </div>\n                                                <div class=\"clearfix\"></div>\n                                            </div>\n\n                                            <div class=\"panel-body\">\n                                                <pre id=\"preAllowedZoneViewerBody\">\n\n                                                </pre>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                </div>\n\n                                <div id=\"mainPanelTabPaneBlockedZones\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n\n                                    <div class=\"well well-sm zone-list-pane\">\n                                        <form class=\"form-inline\">\n                                            <div class=\"form-group\" style=\"width: 100%\">\n                                                <input type=\"text\" class=\"form-control\" style=\"width: inherit;\" id=\"txtBlockZone\" placeholder=\"example.com\">\n                                            </div>\n                                            <div class=\"form-group\">\n                                                <button id=\"btnBlockZone\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Block\" onclick=\"blockZone(); return false;\">Block</button>\n                                                <button id=\"btnBrowseBlockZone\" type=\"button\" class=\"btn btn-default\" data-loading-text=\"Browse\" onclick=\"refreshBlockedZonesList($('#txtBlockZone').val());\">Browse</button>\n                                            </div>\n                                        </form>\n\n                                        <div id=\"lstBlockedZones\" class=\"zones\">\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divBlockedZoneViewer\" class=\"zone-viewer-pane\">\n                                        <div class=\"panel panel-default\">\n                                            <div class=\"panel-heading\" style=\"height: 36px; padding: 4px 6px;\">\n                                                <div id=\"txtBlockedZoneViewerTitle\" class=\"pull-left\" style=\"padding: 4px;\">technitium.com</div>\n                                                <div class=\"form-inline pull-right\">\n                                                    <button id=\"btnImportBlockedZone\" type=\"button\" class=\"btn btn-default\" data-loading-text=\"Import\" onclick=\"resetImportBlockedZonesModal();\" data-toggle=\"modal\" data-target=\"#modalImportBlockedZones\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Import</button>\n                                                    <button id=\"btnExportBlockedZone\" type=\"button\" class=\"btn btn-default\" data-loading-text=\"Export\" onclick=\"exportBlockedZones();\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Export</button>\n                                                    <button id=\"btnDeleteBlockedZone\" type=\"button\" class=\"btn btn-warning\" data-loading-text=\"Delete\" onclick=\"deleteBlockedZone();\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Delete</button>\n                                                    <button id=\"btnFlushBlockedZone\" type=\"button\" class=\"btn btn-danger\" data-loading-text=\"Flush\" onclick=\"flushBlockedZone();\" style=\"font-size: 12px; padding: 4px 6px; width: 50px;\">Flush</button>\n                                                </div>\n                                                <div class=\"clearfix\"></div>\n                                            </div>\n\n                                            <div class=\"panel-body\">\n                                                <pre id=\"preBlockedZoneViewerBody\">\n\n                                                </pre>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                </div>\n\n                                <div id=\"mainPanelTabPaneApps\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n                                    <div id=\"divViewAppsLoader\" style=\" display: none; margin-top: 10px; height: 400px;\"></div>\n\n                                    <div id=\"divViewApps\">\n                                        <div class=\"form-inline\">\n                                            <div style=\"float: right;\">\n                                                <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showStoreAppsModal();\">App Store</button>\n                                                <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showInstallAppModal();\">Install</button>\n                                            </div>\n                                            <div style=\"clear: both;\"></div>\n                                        </div>\n\n                                        <table id=\"tableApps\" class=\"table table-hover\">\n                                            <thead>\n                                                <tr>\n                                                    <th style=\"min-width: 100px;\"><a href=\"#\" onclick=\"sortTable('tableAppsBody', 0); return false;\">Installed Apps</a></th>\n                                                    <th style=\"width: 96px;\"></th>\n                                                </tr>\n                                            </thead>\n                                            <tbody id=\"tableAppsBody\">\n                                            </tbody>\n                                            <tfoot id=\"tableAppsFooter\">\n                                                <tr><td colspan=\"3\"><b>Total Apps: 0</b></td></tr>\n                                            </tfoot>\n                                        </table>\n                                    </div>\n                                </div>\n\n                                <div id=\"mainPanelTabPaneDnsClient\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n\n                                    <form class=\"form-inline well\" style=\"padding-bottom: 6px; margin-bottom: 15px;\">\n                                        <div>\n                                            <div class=\"form-group\">\n                                                <label for=\"txtDnsClientNameServer\">Server</label>\n                                                <div class=\"input-group dropdown\">\n                                                    <input type=\"text\" class=\"form-control dropdown-toggle\" style=\"min-width: 270px; border-right: 0px;\" id=\"txtDnsClientNameServer\" value=\"This Server {this-server}\">\n                                                    <ul id=\"optDnsClientNameServers\" class=\"dropdown-menu\" style=\"max-height: 500px; overflow-y: scroll;\">\n                                                    </ul>\n                                                    <span role=\"button\" class=\"input-group-addon dropdown-toggle\" style=\"background-color: white;\" data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\"><span class=\"caret\"></span></span>\n                                                </div>\n                                            </div>\n\n                                            <div class=\"form-group\">\n                                                <label for=\"txtDnsClientDomain\">Domain</label>\n                                                <input type=\"text\" class=\"form-control\" style=\"min-width: 295px;\" id=\"txtDnsClientDomain\" placeholder=\"example.com\">\n                                            </div>\n\n                                            <div class=\"form-group\">\n                                                <label for=\"optDnsClientType\">Type</label>\n                                                <select class=\"form-control\" id=\"optDnsClientType\" style=\"padding-left: 6px; padding-right: 0px;\">\n                                                    <option>A</option>\n                                                    <option>NS</option>\n                                                    <option>CNAME</option>\n                                                    <option>SOA</option>\n                                                    <option>PTR</option>\n                                                    <option>MX</option>\n                                                    <option>TXT</option>\n                                                    <option>RP</option>\n                                                    <option>AAAA</option>\n                                                    <option>SRV</option>\n                                                    <option>NAPTR</option>\n                                                    <option>DNAME</option>\n                                                    <option>DS</option>\n                                                    <option>SSHFP</option>\n                                                    <option>RRSIG</option>\n                                                    <option>NSEC</option>\n                                                    <option>DNSKEY</option>\n                                                    <option>NSEC3</option>\n                                                    <option>NSEC3PARAM</option>\n                                                    <option>TLSA</option>\n                                                    <option>ZONEMD</option>\n                                                    <option>SVCB</option>\n                                                    <option>HTTPS</option>\n                                                    <option>URI</option>\n                                                    <option>CAA</option>\n                                                    <option>ANY</option>\n                                                    <option>AXFR</option>\n                                                    <option>ANAME</option>\n                                                </select>\n                                            </div>\n\n                                            <div class=\"form-group\">\n                                                <label for=\"optDnsClientProtocol\">DNS-over-</label>\n                                                <select class=\"form-control\" id=\"optDnsClientProtocol\" style=\"padding-left: 6px; padding-right: 0px;\">\n                                                    <option>UDP</option>\n                                                    <option>TCP</option>\n                                                    <option>TLS</option>\n                                                    <option>HTTPS</option>\n                                                    <option>QUIC</option>\n                                                </select>\n                                            </div>\n\n                                            <div class=\"form-group\">\n                                                <label for=\"txtDnsClientEDnsClientSubnet\">EDNS Client Subnet</label>\n                                                <input type=\"text\" class=\"form-control\" style=\"min-width: 240px;\" id=\"txtDnsClientEDnsClientSubnet\">\n                                            </div>\n\n                                            <div class=\"form-group\">\n                                                <div class=\"checkbox\">\n                                                    <label>\n                                                        <input type=\"checkbox\" id=\"chkDnsClientDnssecValidation\"> Enable DNSSEC Validation\n                                                    </label>\n                                                </div>\n                                            </div>\n                                        </div>\n                                        <div>\n                                            <div class=\"form-group\">\n                                                <button type=\"submit\" class=\"btn btn-primary\" id=\"btnDnsClientResolve\" style=\"padding: 2px 0px; width: 100px;\" data-loading-text=\"Resolving...\" onclick=\"resolveQuery(); return false;\">Resolve</button>\n                                                <button type=\"button\" class=\"btn btn-warning\" id=\"btnDnsClientImport\" style=\"padding: 2px 0px; width: 100px;\" data-loading-text=\"Importing...\" onclick=\"resolveQuery(true);\">Import</button>\n                                                <select id=\"optDnsClientClusterNode\" class=\"form-control cluster-node-dropdown\"></select>\n                                            </div>\n                                        </div>\n                                    </form>\n\n                                    <div id=\"divDnsClientLoader\" style=\"margin-top: 15px; height: 300px;\"></div>\n\n                                    <div id=\"divDnsClientOutputAccordion\" style=\"margin-bottom: 0px; display: none;\" class=\"panel-group\" role=\"tablist\" aria-multiselectable=\"true\">\n                                        <div class=\"panel panel-default\">\n                                            <div class=\"panel-heading\" role=\"tab\" id=\"divDnsClientFinalResponseHeading\">\n                                                <h4 class=\"panel-title\">\n                                                    <a role=\"button\" data-toggle=\"collapse\" data-parent=\"#divDnsClientOutputAccordion\" href=\"#divDnsClientFinalResponseCollapse\" aria-expanded=\"true\" aria-controls=\"divDnsClientFinalResponseCollapse\">\n                                                        Response\n                                                    </a>\n                                                </h4>\n                                            </div>\n                                            <div id=\"divDnsClientFinalResponseCollapse\" class=\"panel-collapse collapse in\" role=\"tabpanel\" aria-labelledby=\"divDnsClientFinalResponseHeading\">\n                                                <div class=\"panel-body\">\n                                                    <pre id=\"preDnsClientFinalResponse\" style=\"margin-bottom: 0px;\"></pre>\n                                                </div>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"divDnsClientRawResponsePanel\" class=\"panel panel-default\">\n                                            <div class=\"panel-heading\" role=\"tab\" id=\"divDnsClientRawResponsesHeading\">\n                                                <h4 class=\"panel-title\">\n                                                    <a class=\"collapsed\" role=\"button\" data-toggle=\"collapse\" data-parent=\"#divDnsClientOutputAccordion\" href=\"#divDnsClientRawResponsesCollapse\" aria-expanded=\"false\" aria-controls=\"divDnsClientRawResponsesCollapse\">\n                                                        Raw Responses (<span id=\"spanDnsClientRawResponsesCount\"></span>)\n                                                    </a>\n                                                </h4>\n                                            </div>\n                                            <div id=\"divDnsClientRawResponsesCollapse\" class=\"panel-collapse collapse\" role=\"tabpanel\" aria-labelledby=\"divDnsClientRawResponsesHeading\">\n                                                <ul id=\"ulDnsClientRawResponsesList\" class=\"list-group\">\n                                                </ul>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div id=\"mainPanelTabPaneSettings\" role=\"tabpanel\" class=\"tab-pane\">\n\n                                    <div id=\"divDnsSettingsLoader\" style=\"margin-top: 10px; height: 400px;\"></div>\n\n                                    <div id=\"divDnsSettings\" style=\"display: none;\">\n                                        <form style=\"margin-top: 10px;\" onsubmit=\"return false;\">\n\n                                            <ul class=\"nav nav-tabs\" role=\"tablist\">\n                                                <li id=\"settingsTabListGeneral\" role=\"presentation\" class=\"active\"><a href=\"#settingsTabPaneGeneral\" aria-controls=\"settingsTabPaneGeneral\" role=\"tab\" data-toggle=\"tab\">General</a></li>\n                                                <li id=\"settingsTabListWebService\" role=\"presentation\"><a href=\"#settingsTabPaneWebService\" aria-controls=\"settingsTabPaneWebService\" role=\"tab\" data-toggle=\"tab\">Web Service</a></li>\n                                                <li id=\"settingsTabListOptionalProtocols\" role=\"presentation\"><a href=\"#settingsTabPaneOptionalProtocols\" aria-controls=\"settingsTabPaneOptionalProtocols\" role=\"tab\" data-toggle=\"tab\">Optional Protocols</a></li>\n                                                <li id=\"settingsTabListTsig\" role=\"presentation\"><a href=\"#settingsTabPaneTsig\" aria-controls=\"settingsTabPaneTsig\" role=\"tab\" data-toggle=\"tab\">TSIG</a></li>\n                                                <li id=\"settingsTabListRecursion\" role=\"presentation\"><a href=\"#settingsTabPaneRecursion\" aria-controls=\"settingsTabPaneRecursion\" role=\"tab\" data-toggle=\"tab\">Recursion</a></li>\n                                                <li id=\"settingsTabListCache\" role=\"presentation\"><a href=\"#settingsTabPaneCache\" aria-controls=\"settingsTabPaneCache\" role=\"tab\" data-toggle=\"tab\">Cache</a></li>\n                                                <li id=\"settingsTabListBlocking\" role=\"presentation\"><a href=\"#settingsTabPaneBlocking\" aria-controls=\"settingsTabPaneBlocking\" role=\"tab\" data-toggle=\"tab\">Blocking</a></li>\n                                                <li id=\"settingsTabListProxyForwarders\" role=\"presentation\"><a href=\"#settingsTabPaneProxyForwarders\" aria-controls=\"settingsTabPaneProxyForwarders\" role=\"tab\" data-toggle=\"tab\">Proxy &amp; Forwarders</a></li>\n                                                <li id=\"settingsTabListLogging\" role=\"presentation\"><a href=\"#settingsTabPaneLogging\" aria-controls=\"settingsTabPaneLogging\" role=\"tab\" data-toggle=\"tab\">Logging</a></li>\n\n                                                <li class=\"pull-right\">\n                                                    <select id=\"optSettingsClusterNode\" class=\"form-control pull-right cluster-node-dropdown\" style=\"margin-left: 0px;\" onchange=\"refreshDnsSettings();\"></select>\n                                                </li>\n                                            </ul>\n\n                                            <div class=\"tab-content\" style=\"min-height: 350px; padding-top: 15px;\">\n                                                <div id=\"settingsTabPaneGeneral\" role=\"tabpanel\" class=\"tab-pane active\">\n                                                    <div id=\"divSettingsGeneralLocalParameters\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsServerDomain\" class=\"col-sm-3 control-label\">DNS Server Domain</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDnsServerDomain\" placeholder=\"domain name\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The primary fully qualified domain name used by this DNS Server to identify itself.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsServerLocalEndPoints\" class=\"col-sm-3 control-label\">DNS Server Local End Points</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtDnsServerLocalEndPoints\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Local end points are the network interface IP addresses and ports you want the DNS Server to listen for requests. The default values work for Windows but on Linux when you have multiple network adapters, you must explicitly specify the network adapter IP addresses here as the local end points.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsServerIPv4SourceAddresses\" class=\"col-sm-3 control-label\">DNS Server IPv4 Source Addresses</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtDnsServerIPv4SourceAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The 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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsServerIPv6SourceAddresses\" class=\"col-sm-3 control-label\">DNS Server IPv6 Source Addresses</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtDnsServerIPv6SourceAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The 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. Note that this option will be used only when <code>Prefer IPv6</code> option is enabled.</div>\n                                                        </div>\n\n                                                        <div>\n                                                            <p>Note! The DNS Server local end point changes will be automatically applied and so you do not need to manually restart the main service.</p>\n                                                            <p>Note! The source adddresses configured above must be the IP addresses that are configured on the local system's network interface. When using source addresses option, its also necessary to ensure that the system has a default route or a specific route for the source address to be able to reach the destination network. When source addresses are not configured, the IP address of the interface with a default route will be used as the source address.</p>\n                                                        </div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralDefaultParameters\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDefaultRecordTtl\" class=\"col-sm-3 control-label\">Default Record TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDefaultRecordTtl\" placeholder=\"TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (default 3600/1h)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The default TTL value to use if not specified when adding or updating records in a Zone.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDefaultNsRecordTtl\" class=\"col-sm-3 control-label\">Default NS Record TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDefaultNsRecordTtl\" placeholder=\"TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (default 14400/4h)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The default TTL value to use if not specified when adding or updating NS records in a Primary Zone.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDefaultSoaRecordTtl\" class=\"col-sm-3 control-label\">Default SOA Record TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDefaultSoaRecordTtl\" placeholder=\"TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (default 900/15m)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The default TTL value to use if not specified when adding or updating SOA records in a Primary Zone.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDefaultResponsiblePerson\" class=\"col-sm-3 control-label\">Default Responsible Person</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDefaultResponsiblePerson\" placeholder=\"email address\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The default SOA Responsible Person email address to use when adding a Primary Zone.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Zone Defaults</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkUseSoaSerialDateScheme\" type=\"checkbox\"> Use SOA Serial Date Scheme\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">The default SOA Serial option to use if not specified when adding a Primary Zone.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtMinSoaRefresh\" class=\"col-sm-3 control-label\">Minimum SOA Refresh</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtMinSoaRefresh\" placeholder=\"TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (default 300/5m)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The minimum Refresh interval to be used by Secondary, Stub, Secondary Forwarder, and Secondary Catalog zones. This minimum value will be used if a zone's SOA Refresh value is less than it.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtMinSoaRetry\" class=\"col-sm-3 control-label\">Minimum SOA Retry</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtMinSoaRetry\" placeholder=\"TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (default 300/5m)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The minimum Retry interval to be used by Secondary, Stub, Secondary Forwarder, and Secondary Catalog zones zones. This minimum value will be used if a zone's SOA Retry value is less than it.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtZoneTransferAllowedNetworks\" class=\"col-sm-3 control-label\">Zone Transfer Allowed Networks</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtZoneTransferAllowedNetworks\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another that are allowed to perform zone transfer for all zones without any TSIG authentication.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtNotifyAllowedNetworks\" class=\"col-sm-3 control-label\">Notify Allowed Networks</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtNotifyAllowedNetworks\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another that are allowed to Notify all Secondary Zones.</div>\n                                                            </div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralDnsApps\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDefaultRecordTtl\" class=\"col-sm-3 control-label\">DNS Apps</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkDnsAppsEnableAutomaticUpdate\" type=\"checkbox\"> Enable Automatic Update\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">DNS server will check for DNS Apps update every day and will automatically download and install the updates.</div>\n                                                            </div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralIpv6\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">IPv6 Support</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkPreferIPv6\" type=\"checkbox\"> Prefer IPv6\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">The DNS Server will use IPv6 for querying whenever possible with this option enabled.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div>Warning! Use this option only if this DNS server has native IPv6 Internet access otherwise it will affect performance. There are many name servers on the Internet that do not respond over IPv6 and thus enabling this option when you are running DNS server in recursive resolver mode (i.e. without any forwarders) may cause frequent operational issues with resolution that may result increase in Server Failure responses.</div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralUdpSocketPool\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">UDP Socket Pool</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableUdpSocketPool\" type=\"checkbox\"> Enable UDP Socket Pool\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">The DNS Server will use UDP socket pool for all outbound DNS-over-UDP requests when enabled.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtUdpSocketPoolExcludedPorts\" class=\"col-sm-3 control-label\">UDP Socket Pool Excluded Ports</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtUdpSocketPoolExcludedPorts\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                                <div style=\"padding-top: 5px;\">Enter port numbers one below other to be excluded from being used by the UDP socket pool.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div>Note! Enabling UDP socket pool provides port randomization for all outbound DNS-over-UDP requests to mitigate spoofing attacks. It is recommended to enable UDP socket pool on Windows platform. On Linux, ports are fairly random and thus socket pool may be enabled if more randomization is desired. The DNS server can detect DNS spoofing attack attempts based on ID mismatch and switch to TCP protocol automatically.</div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralEDns\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtEdnsUdpPayloadSize\" class=\"col-sm-3 control-label\">EDNS UDP Payload Size</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtEdnsUdpPayloadSize\" placeholder=\"size\" style=\"width: 100px; display: inline;\">\n                                                                <span>bytes (valid range 512-4096; default 1232)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum UDP payload size that can be used to avoid IP fragmentation.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralDnssec\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">DNSSEC</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkDnssecValidation\" type=\"checkbox\" checked> Enable DNSSEC Validation\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">The DNS Server will validate all responses from name servers or forwarders when this option is enabled.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div>\n                                                            <p>Warning! Devices that do not have a real-time clock and rely on NTP when booting (e.g. Raspberry Pi), enabling DNSSEC validation will cause failure to resolve the NTP server domain name thus causing the DNS server to fail to validate all other domain names too due to invalid system date/time. To fix this issue, just create a Conditional Forwarder zone for the NTP server domain name (e.g. ntp.org) with forwarder set to <code>this-server</code> and Enable DNSSEC Validation option unchecked. This conditional forwarder zone will disable DNSSEC validation for the NTP server domain name and allow the device to update its system data/time on boot.</p>\n                                                            <p>Warning! When forwarders are configured, DNSSEC validation will work only if the forwarders are security aware i.e. can respond to DNSSEC requests correctly.</p>\n                                                            <p>Note! Enabling DNSSEC may increase delays in resolving domain names when the cache is initially empty. As the cache fills up, the performance will be normal as expected.</p>\n                                                        </div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralEDnsClientSubnet\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">EDNS Client Subnet (ECS)</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEDnsClientSubnet\" type=\"checkbox\"> Enable EDNS Client Subnet\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">The DNS Server will use the public IP address of the request with a prefix length, or the existing Client Subnet option from the request.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtEDnsClientSubnetIPv4PrefixLength\" class=\"col-sm-3 control-label\">ECS IPv4 Prefix Length</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtEDnsClientSubnetIPv4PrefixLength\" placeholder=\"prefix\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 0-32; default 24)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The IPv4 prefix length to define the client subnet.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtEDnsClientSubnetIPv6PrefixLength\" class=\"col-sm-3 control-label\">ECS IPv6 Prefix Length</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtEDnsClientSubnetIPv6PrefixLength\" placeholder=\"prefix\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 0-64; default 56)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The IPv6 prefix length to define the client subnet.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtEDnsClientSubnetIpv4Override\" class=\"col-sm-3 control-label\">ECS IPv4 Override</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtEDnsClientSubnetIpv4Override\" placeholder=\"network address\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The IPv4 network address that must be used as ECS for all outbound requests overriding client's actual subnet.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtEDnsClientSubnetIpv6Override\" class=\"col-sm-3 control-label\">ECS IPv6 Override</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtEDnsClientSubnetIpv6Override\" placeholder=\"network address\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The IPv6 network address that must be used as ECS for all outbound requests overriding client's actual subnet.</div>\n                                                        </div>\n\n                                                        <div>\n                                                            <p>Warning! EDNS Client Subnet (ECS) option when enabled will compromises user's privacy since the DNS server will send the user's public IP network subnet to name servers or forwarders when resolving requests. When not using encrypted DNS protocols, this information can also be read passively by anyone on the network.</p>\n                                                            <p>Note! EDNS Client Subnet (ECS) option allows passing the user's client subnet information to name servers or forwarders so that the response may contain IP addresses of servers closer to the user's geographic region. EDNS Client Subnet (ECS) option thus is only useful when the DNS server is hosted in a geographically different region compared to the users that are configured to use it.</p>\n                                                            <p>Note! Enabling EDNS Client Subnet (ECS) option will significantly increase the DNS server's memory usage since the server will have to cache data for each client subnet separately. It will also increase cache misses since DNS server will have to resolve requests and cache them for each client subnet separately.</p>\n                                                        </div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralRateLimiting\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Queries Per Minute (QPM) Limits (IPv4)</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th style=\"width: 100px;\">IPv4 Prefix</th>\n                                                                            <th>UDP Limit</th>\n                                                                            <th>TCP Limit</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addQpmPrefixLimitsIPv4Row('', '', '');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableQpmPrefixLimitsIPv4\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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 <code>0</code> to allow unlimited queries.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Queries Per Minute (QPM) Limits (IPv6)</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th style=\"width: 100px;\">IPv6 Prefix</th>\n                                                                            <th>UDP Limit</th>\n                                                                            <th>TCP Limit</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addQpmPrefixLimitsIPv6Row('', '', '');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableQpmPrefixLimitsIPv6\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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 <code>0</code> to allow unlimited queries.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtQpmLimitSampleMinutes\" class=\"col-sm-3 control-label\">QPM Sample Size</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtQpmLimitSampleMinutes\" placeholder=\"sample\" style=\"width: 100px; display: inline;\">\n                                                                <span>minutes (valid range 1-60; default 5)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The sample size in minutes to be used for limiting queries per client.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtQpmLimitUdpTruncation\" class=\"col-sm-3 control-label\">QPM Limit UDP Truncation</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtQpmLimitUdpTruncation\" placeholder=\"%\" style=\"width: 100px; display: inline;\">\n                                                                <span>% (valid range 0-100; default 50)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtQpmLimitBypassList\" class=\"col-sm-3 control-label\">QPM Limit Bypass List</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtQpmLimitBypassList\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another that are allowed to bypass the QPM limit.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div>\n                                                            <p>Note! Queries Per Minute (QPM) feature will limit requests from a client subnet based on its IP address and the specified subnet prefix lengths except for loopback IP addresses. The QPM limit configured will be compared with the average count from the sample size which means a client may exceed the QPM limit for a given minute but won't exceed for the given sample size in minutes. Rate limited clients will be listed in orange color on the dashboard top clients table.</p>\n                                                            <p>Note! The configured TCP limits apply to the DNS-over-TCP protocol service as well as to the DNS-over-TLS, DNS-over-HTTPS and DNS-over-QUIC optional protocol services.</p>\n                                                        </div>\n                                                    </div>\n\n                                                    <div id=\"divSettingsGeneralAdvancedOptions\" class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtClientTimeout\" class=\"col-sm-3 control-label\">Client Timeout</label>\n                                                            <div class=\"col-sm-7\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtClientTimeout\" placeholder=\"timeout\" style=\"width: 100px; display: inline;\">\n                                                                <span>milliseconds (valid range 1000-10000; default 2000)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The amount of time the DNS server must wait before responding with a <code>ServerFailure</code> response to a client request when no answer is available.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtTcpSendTimeout\" class=\"col-sm-3 control-label\">TCP Send Timeout</label>\n                                                            <div class=\"col-sm-7\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtTcpSendTimeout\" placeholder=\"timeout\" style=\"width: 100px; display: inline;\">\n                                                                <span>milliseconds (valid range 1000-90000; default 10000)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum amount of time the DNS Server 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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtTcpReceiveTimeout\" class=\"col-sm-3 control-label\">TCP Receive Timeout</label>\n                                                            <div class=\"col-sm-7\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtTcpReceiveTimeout\" placeholder=\"timeout\" style=\"width: 100px; display: inline;\">\n                                                                <span>milliseconds (valid range 1000-90000; default 10000)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum amount of time the DNS Server 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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtQuicIdleTimeout\" class=\"col-sm-3 control-label\">QUIC Idle Timeout</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtQuicIdleTimeout\" placeholder=\"timeout\" style=\"width: 100px; display: inline;\">\n                                                                <span>milliseconds (valid range 1000-90000; default 60000)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The time interval after which an idle QUIC connection will be closed. This option applies only to QUIC transport protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtQuicMaxInboundStreams\" class=\"col-sm-3 control-label\">QUIC Max Inbound Streams</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtQuicMaxInboundStreams\" placeholder=\"100\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 1-1000; default 100)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The max number of inbound bidirectional streams that can be accepted per QUIC connection. This option applies only to QUIC transport protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtListenBacklog\" class=\"col-sm-3 control-label\">Listen Backlog</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtListenBacklog\" placeholder=\"100\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 100)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum number of pending inbound connections. This option applies to TCP, TLS, TcpProxy, and QUIC transport protocols.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtMaxConcurrentResolutionsPerCore\" class=\"col-sm-3 control-label\">Max Concurrent Resolutions</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtMaxConcurrentResolutionsPerCore\" placeholder=\"100\" style=\"width: 100px; display: inline;\">\n                                                                <span>per CPU core (default 100)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum number of concurrent async outbound resolutions that should be done per CPU core.</div>\n                                                        </div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneWebService\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtWebServiceLocalAddresses\" class=\"col-sm-3 control-label\">Web Service Local Addresses</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtWebServiceLocalAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Local addresses are the network interface IP addresses you want the web service to listen for requests. ANY addresses (0.0.0.0 &amp; [::]) cannot be used together with unicast IP addresses. The web server uses dual-mode sockets by default so the IPv6 ANY address ([::]) works for IPv4 too. The default values work for most scenarios so, do not change these defaults unless you have a requirement for the web service to listen on specific networks. Configured unicast IP addresses will be included as Subject Alternative Name (SAN) in the self signed TLS certificate.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtWebServiceHttpPort\" class=\"col-sm-3 control-label\">Web Service HTTP Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtWebServiceHttpPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 5380)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the TCP port number for this web console over HTTP protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">HTTPS Options</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkWebServiceEnableTls\" type=\"checkbox\"> Enable HTTPS\n                                                                    </label>\n                                                                </div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkWebServiceEnableHttp3\" type=\"checkbox\"> Enable HTTP/3\n                                                                    </label>\n                                                                </div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkWebServiceHttpToTlsRedirect\" type=\"checkbox\"> Enable HTTP to HTTPS Redirection\n                                                                    </label>\n                                                                </div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkWebServiceUseSelfSignedTlsCertificate\" type=\"checkbox\"> Use A Self Signed TLS Certificate When TLS Certificate File Path Is Unspecified\n                                                                    </label>\n                                                                </div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtWebServiceTlsPort\" class=\"col-sm-3 control-label\">Web Service HTTPS Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtWebServiceTlsPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 53443)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the TCP port number for this web console over TLS protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtWebServiceTlsCertificatePath\" class=\"col-sm-3 control-label\">TLS Certificate File Path</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtWebServiceTlsCertificatePath\" placeholder=\"Web Service TLS Certificate File Path On Server\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify a PKCS #12 certificate (.pfx or .p12) file path on the server. The path can be relative to the DNS server's config folder. The certificate must contain private key.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtWebServiceTlsCertificatePassword\" class=\"col-sm-3 control-label\">TLS Certificate Password</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"password\" class=\"form-control\" id=\"txtWebServiceTlsCertificatePassword\" placeholder=\"Web Service TLS Certificate Password\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Enter the certificate (.pfx) password, if any.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtWebServiceRealIpHeader\" class=\"col-sm-3 control-label\">Real IP Header</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtWebServiceRealIpHeader\" placeholder=\"X-Real-IP\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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.</div>\n                                                        </div>\n\n                                                        <div>\n                                                            <p>Note! The web service port changes will be automatically applied and so you do not need to manually restart the main service. The TLS certificate too will be automatically reloaded when the certificate file's date modified property on disk changes. This web page will be automatically redirected to the new web console URL after saving settings. The HTTPS protocol will be enabled only when a TLS certificate is configured.</p>\n                                                            <p>When using a reverse proxy with the Web Service, you need to add <code id=\"lblWebServiceRealIpHeader\">X-Real-IP</code> header to the proxy request with the IP address of the client to allow the Web server to know the real IP address of the client originating the request. For example, if you are using nginx as the reverse proxy, you can add <code id=\"lblWebServiceRealIpNginx\">proxy_set_header X-Real-IP $remote_addr;</code> to make it work.</p>\n                                                            <p>The web service uses Kestrel web server which supports both HTTP/2 and HTTP/3 protocols when TLS certificate is configured. HTTP/3 protocol support is not available on all platforms. On Windows, it is available only on Windows 11 (build 22000 or later) and Windows Server 2022. On Linux, it requires <code>libmsquic</code> to be installed.</p>\n                                                            <p>Note! The web service will always bind to <code>[::]</code> local address for HTTP/3 protocol since this is how the <code>libmsquic</code> library is designed to work.</p>\n                                                            <p>Use the following openssl command to convert your TLS certificate that is in PEM format to PKCS #12 certificate (.pfx) format:</p>\n                                                            <pre>openssl pkcs12 -export -out \"example.com.pfx\" -inkey \"privkey.pem\" -in \"cert.pem\" -certfile \"chain.pem\"</pre>\n                                                        </div>\n\n                                                        <div style=\"margin-top: 10px;\"><a href=\"https://blog.technitium.com/2023/02/configuring-dns-over-quic-and-https3.html\" target=\"_blank\">Help: Configuring DNS-over-QUIC and HTTPS/3 For Technitium DNS Server</a></div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneOptionalProtocols\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Optional DNS Server Protocols</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableDnsOverUdpProxy\" type=\"checkbox\"> Enable DNS-over-UDP-PROXY\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to accept DNS-over-UDP-PROXY requests. It implements the <a href=\"https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt\" target=\"_blank\">PROXY Protocol</a> for both version 1 &amp; 2 over UDP datagram. It is mandatory to configure <b>Reverse Proxy Network ACL</b> below to allow requests coming from your reverse proxy server.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableDnsOverTcpProxy\" type=\"checkbox\"> Enable DNS-over-TCP-PROXY\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to accept DNS-over-TCP-PROXY requests. It implements the <a href=\"https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt\" target=\"_blank\">PROXY Protocol</a> for both version 1 &amp; 2 over TCP connection. It is mandatory to configure <b>Reverse Proxy Network ACL</b> below to allow requests coming from your reverse proxy server.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableDnsOverHttp\" type=\"checkbox\"> Enable DNS-over-HTTP\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to accept DNS-over-HTTP requests. It must be used with a TLS terminating reverse proxy like nginx. It is mandatory to configure <b>Reverse Proxy Network ACL</b> below to allow 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 when DNS-over-HTTP port is set to 80.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableDnsOverTls\" type=\"checkbox\"> Enable DNS-over-TLS\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to accept DNS-over-TLS requests.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableDnsOverHttps\" type=\"checkbox\"> Enable DNS-over-HTTPS\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to accept DNS-over-HTTPS requests.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableDnsOverHttp3\" type=\"checkbox\"> Enable DNS-over-HTTP/3\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to accept DNS-over-HTTP/3 requests.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableDnsOverQuic\" type=\"checkbox\"> Enable DNS-over-QUIC\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to accept DNS-over-QUIC requests.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsOverUdpProxyPort\" class=\"col-sm-3 control-label\">DNS-over-UDP-PROXY Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtDnsOverUdpProxyPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 538)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the UDP port number for DNS-over-UDP-PROXY protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsOverTcpProxyPort\" class=\"col-sm-3 control-label\">DNS-over-TCP-PROXY Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtDnsOverTcpProxyPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 538)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the TCP port number for DNS-over-TCP-PROXY protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsOverHttpPort\" class=\"col-sm-3 control-label\">DNS-over-HTTP Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtDnsOverHttpPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 80)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the TCP port number for DNS-over-HTTP protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsOverTlsPort\" class=\"col-sm-3 control-label\">DNS-over-TLS Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtDnsOverTlsPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 853)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the TCP port number for DNS-over-TLS protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsOverHttpsPort\" class=\"col-sm-3 control-label\">DNS-over-HTTPS Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtDnsOverHttpsPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 443)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the TCP port number for DNS-over-HTTPS protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsOverQuicPort\" class=\"col-sm-3 control-label\">DNS-over-QUIC Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtDnsOverQuicPort\" placeholder=\"port\" style=\"width: 100px; display: inline;\">\n                                                                <span>(default 853)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify the UDP port number for DNS-over-QUIC protocol.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtReverseProxyNetworkACL\" class=\"col-sm-3 control-label\">Reverse Proxy Network ACL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtReverseProxyNetworkACL\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Configure the ACL above to allow 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 <code>!</code> character at the start to deny access, e.g. <code>!192.168.10.0/24</code> 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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsTlsCertificatePath\" class=\"col-sm-3 control-label\">TLS Certificate File Path</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDnsTlsCertificatePath\" placeholder=\"DNS Service TLS Certificate File Path On Server\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Specify a PKCS #12 certificate (.pfx or .p12) file path on the server. The path can be relative to the DNS server's config folder. The certificate must contain private key.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsTlsCertificatePassword\" class=\"col-sm-3 control-label\">TLS Certificate Password</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"password\" class=\"form-control\" id=\"txtDnsTlsCertificatePassword\" placeholder=\"DNS Service TLS Certificate Password\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Enter the certificate (.pfx) password, if any.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDnsOverHttpRealIpHeader\" class=\"col-sm-3 control-label\">Real IP Header</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDnsOverHttpRealIpHeader\" placeholder=\"X-Real-IP\" maxlength=\"255\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The HTTP header that must be used to read client's actual IP address when the request comes from a reverse proxy. The specified header will be read only when the request IP address is allowed by the <b>Reverse Proxy Network ACL</b>.</div>\n                                                        </div>\n\n                                                        <div>\n                                                            <p>Note! These optional DNS server protocol changes will be automatically applied and so you do not need to manually restart the main service. The TLS certificate too will be automatically reloaded when the certificate file's date modified property on disk changes. The DNS-over-TLS, DNS-over-QUIC, and DNS-over-HTTPS protocols will be enabled only when a TLS certificate is configured.</p>\n                                                            <p>These optional DNS server protocols are used to host these as a service. You do not need to enable these optional protocols to use them with Forwarders or Conditional Forwarder Zones.</p>\n                                                            <p>For DNS-over-HTTP, use <code>http://<span id=\"lblDoHHost\">localhost:8053</span>/dns-query</code> with a TLS terminating reverse proxy like nginx. For DNS-over-TLS, use <code id=\"lblDoTHost\">tls-certificate-domain:853</code>, for DNS-over-QUIC, use <code id=\"lblDoQHost\">tls-certificate-domain:853</code>, and for DNS-over-HTTPS use <code>https://<span id=\"lblDoHsHost\">tls-certificate-domain</span>/dns-query</code> to configure supported DNS clients.</p>\n                                                            <p>When using a reverse proxy with the DNS-over-HTTP service, you need to add <code id=\"lblDnsOverHttpRealIpHeader\">X-Real-IP</code> header to the proxy request with the IP address of the client to allow the DNS server to know the real IP address of the client originating the request. For example, if you are using nginx as the reverse proxy, you can add <code id=\"lblDnsOverHttpRealIpNginx\">proxy_set_header X-Real-IP $remote_addr;</code> to make it work.</p>\n                                                            <p>DNS-over-QUIC protocol support is not available on all platforms. On Windows, it is available only on Windows 11 (build 22000 or later) and Windows Server 2022. On Linux, it requires <code>libmsquic</code> to be installed.</p>\n                                                            <p>Note! The DNS-over-HTTP/3 protocol will always bind to <code>[::]</code> local address since this is how the <code>libmsquic</code> library is designed to work.</p>\n                                                            <p>Use the following openssl command to convert your TLS certificate that is in PEM format to PKCS #12 certificate (.pfx) format:</p>\n                                                            <pre>openssl pkcs12 -export -out \"example.com.pfx\" -inkey \"privkey.pem\" -in \"cert.pem\" -certfile \"chain.pem\"</pre>\n                                                        </div>\n\n                                                        <div style=\"margin-top: 10px;\"><a href=\"https://blog.technitium.com/2020/07/how-to-host-your-own-dns-over-https-and.html\" target=\"_blank\">Help: How To Host Your Own DNS-over-HTTPS, DNS-over-TLS, And DNS-over-QUIC Services</a></div>\n                                                        <div style=\"margin-top: 10px;\"><a href=\"https://blog.technitium.com/2023/02/configuring-dns-over-quic-and-https3.html\" target=\"_blank\">Help: Configuring DNS-over-QUIC and HTTPS/3 For Technitium DNS Server</a></div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneTsig\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"tableTsigKeys\" class=\"col-sm-2 control-label\">TSIG Keys</label>\n                                                            <div class=\"col-sm-10\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th>Key Name</th>\n                                                                            <th>Shared Secret</th>\n                                                                            <th>Algorithm</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addTsigKeyRow('', '', 'hmac-sha256');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableTsigKeys\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-2 col-sm-10\" style=\"padding-top: 5px;\">The shared secret can be a base64 string or a literal string. Keep the shared secret empty if you want to auto generate a strong key.</div>\n                                                        </div>\n\n                                                        <div>Note! You will need to configure these TSIG keys names for zone transfer in the zone options and in the secondary zone SOA record options separately.</div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneRecursion\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Recursion</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdRecursion\" id=\"rdRecursionDeny\" value=\"Deny\">\n                                                                        Deny Recursion\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Disables recursion so that this DNS Server works as authoritative only.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdRecursion\" id=\"rdRecursionAllow\" value=\"Allow\">\n                                                                        Allow Recursion\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enables recursion to allow this DNS Server to resolve any domain name.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdRecursion\" id=\"rdRecursionAllowOnlyForPrivateNetworks\" value=\"AllowOnlyForPrivateNetworks\" checked>\n                                                                        Allow Recursion Only For Private Networks (default)\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Select this option if you want to support recursion only on private networks. Any recursive request from a public network will be refused.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdRecursion\" id=\"rdRecursionUseSpecifiedNetworkACL\" value=\"UseSpecifiedNetworkACL\">\n                                                                        Use Specified Network Access Control List (ACL)\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Select this option to specify networks that must be allowed or denied recursion.</div>\n                                                                </div>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-6\">\n                                                                <label for=\"txtRecursionNetworkACL\" class=\"control-label\">Network Access Control List (ACL)</label>\n                                                                <textarea id=\"txtRecursionNetworkACL\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another to allow access. Add <code>!</code> character at the start to deny access, e.g. <code>!192.168.10.0/24</code> 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.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div>Note! Disable recursion if you wish this server to act only as authoritative name server for the configured zones.</div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Recursive Resolver</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkRandomizeName\" type=\"checkbox\"> Randomize Name\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enables <a href=\"https://datatracker.ietf.org/doc/draft-vixie-dnsext-dns0x20/\" target=\"_blank\">QNAME case randomization</a> when using UDP as the transport protocol to improve security.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkQnameMinimization\" type=\"checkbox\"> QNAME Minimization\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enables <a href=\"https://datatracker.ietf.org/doc/rfc9156/\" target=\"_blank\">QNAME minimization</a> for recursive resolution to improve privacy.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div>Warning! Enabling the <b>Randomize Name</b> option may cause some domain names to fail to resolve due to their name servers dropping the requests or sending the QNAME in response with a different case causing mismatch. The DNS server can already detect DNS spoofing attack attempts and switch to TCP protocol automatically so its safe to not use this feature.</div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtResolverRetries\" class=\"col-sm-3 control-label\">Resolver Retries</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtResolverRetries\" placeholder=\"retries\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 1-10; default 2)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The total number of retries the recursive resolver must do per name server.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtResolverTimeout\" class=\"col-sm-3 control-label\">Resolver Timeout</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtResolverTimeout\" placeholder=\"timeout\" style=\"width: 100px; display: inline;\">\n                                                                <span>milliseconds (valid range 1000-10000; default 1500)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The amount of time the recursive resolver must wait between retries.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtResolverConcurrency\" class=\"col-sm-3 control-label\">Resolver Concurrency</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtResolverConcurrency\" placeholder=\"count\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 1-4; default 2)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The number of concurrent requests that should be sent by the recursive resolver to the name servers.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtResolverMaxStackCount\" class=\"col-sm-3 control-label\">Resolver Max Stack Count</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtResolverMaxStackCount\" placeholder=\"count\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 10-30; default 16)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum stack count the recursive resolver must use for resolving a domain name.</div>\n                                                        </div>\n\n                                                        <div style=\"margin-top: 10px;\">Note! The DNS Server supports EDNS and thus all outbound recursive resolution requests will have an OPT record for it in the additional section. If a name server does not respond to a request containing OPT record, the recursive resolver will retry again without the OPT record when possible. This means that the number of retries attempted per name server can be Resolver Retries value multiplied by two for certain cases.</div>\n                                                        <div style=\"margin-top: 10px;\">Note! The DNS server uses Epsilon-Greedy machine learning algorithm and will automatically learn which of the name servers are answering faster without errors and will use those name servers most of the time. Since each domain name has a different set of name servers, it may take a while before the algorithm learns about them.</div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneCache\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">DNS Cache</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkSaveCache\" type=\"checkbox\"> Save Cache To Disk\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">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.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div>Note! The DNS server will attempt to save cache to disk when it stops which may take time depending on the cache size. If the DNS server takes a lot of time to stop then it may lead to the OS killing the DNS server process causing an incomplete cache to be stored on disk.</div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Serve Stale</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkServeStale\" type=\"checkbox\"> Enable Serve Stale\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable the <a href=\"https://datatracker.ietf.org/doc/rfc8767/\" target=\"_blank\">Serve Stale</a> feature to improve resiliency by using expired or stale records in cache to respond when the DNS server is unable to reach the upstream or authoritative name servers to refresh the expired records before the Max Wait Time configured below.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtServeStaleTtl\" class=\"col-sm-3 control-label\">Serve Stale TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtServeStaleTtl\" placeholder=\"seconds\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (recommended 259200/3d)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtServeStaleAnswerTtl\" class=\"col-sm-3 control-label\">Serve Stale Answer TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtServeStaleAnswerTtl\" placeholder=\"seconds\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (valid range 0-300/5m; recommended 30)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtServeStaleResetTtl\" class=\"col-sm-3 control-label\">Serve Stale Reset TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtServeStaleResetTtl\" placeholder=\"seconds\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (valid range 10-900/15m; recommended 30)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtServeStaleMaxWaitTime\" class=\"col-sm-3 control-label\">Serve Stale Max Wait Time</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtServeStaleMaxWaitTime\" placeholder=\"milliseconds\" style=\"width: 100px; display: inline;\">\n                                                                <span>milliseconds (valid range 0-1800; default 1800)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCacheMaximumEntries\" class=\"col-sm-3 control-label\">Cache Maximum Entries</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtCacheMaximumEntries\" placeholder=\"entries\" style=\"width: 125px; display: inline;\">\n                                                                <span>(default 10000; set 0 for unlimited entries)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum number of entries that the cache can store. A relevant value should be configured by monitoring the Cache entries value on Dashboard and the server's memory usage to limit the amount of RAM used by the DNS server. A cache entry is a complete Resource Record Set (RR Set) which is a group of records with the same type for a given domain name. When a value is configured, the DNS server will trigger a clean up operation every few minutes and remove least recently used entries to maintain the maximum allowed entries in cache.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCacheMinimumRecordTtl\" class=\"col-sm-3 control-label\">Cache Minimum TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtCacheMinimumRecordTtl\" placeholder=\"min TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (recommended 10)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The minimum TTL value that a record can have in the cache. Set a value to make sure that the records with TTL value less than that stays in cache for a minimum duration.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCacheMaximumRecordTtl\" class=\"col-sm-3 control-label\">Cache Maximum TTL</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtCacheMaximumRecordTtl\" placeholder=\"max TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (default 604800/1w)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum TTL value that a record can have in the cache. Set a lower value to allow the records to expire early.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCacheNegativeRecordTtl\" class=\"col-sm-3 control-label\">Cache Negative TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtCacheNegativeRecordTtl\" placeholder=\"-ve TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (recommended 300/5m)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The negative TTL value to use when there is no SOA MINIMUM value available. Negative caching stores records in cache for <code>NXDOMAIN</code> and <code>NODATA</code> responses.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCacheFailureRecordTtl\" class=\"col-sm-3 control-label\">Cache Failure TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtCacheFailureRecordTtl\" placeholder=\"fail TTL\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (recommended 10)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The failure TTL value to be used for caching failure responses. This allows storing failure record in cache and prevent frequent recursive resolution requests to the name servers that are responding with <code>ServerFailure</code> or failing to respond.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCachePrefetchEligibility\" class=\"col-sm-3 control-label\">Prefetch Eligibility</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtCachePrefetchEligibility\" placeholder=\"eligibility\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (recommended 2)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The minimum initial TTL value of a record needed to be eligible for prefetching.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCachePrefetchTrigger\" class=\"col-sm-3 control-label\">Prefetch Trigger</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtCachePrefetchTrigger\" placeholder=\"trigger\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (recommended 9; set 0 to disable prefetching &amp; auto prefetching)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">A record with TTL value less than trigger value will initiate prefetch operation immediately for itself.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCachePrefetchSampleIntervalInMinutes\" class=\"col-sm-3 control-label\">Auto Prefetch Sampling</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtCachePrefetchSampleIntervalInMinutes\" placeholder=\"interval\" style=\"width: 100px; display: inline;\">\n                                                                <span>minutes (valid range 1-60; default 5)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The interval to sample eligible domain names from last hour stats for auto prefetch.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtCachePrefetchSampleEligibilityHitsPerHour\" class=\"col-sm-3 control-label\">Auto Prefetch Eligibility</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtCachePrefetchSampleEligibilityHitsPerHour\" placeholder=\"hits\" style=\"width: 100px; display: inline;\">\n                                                                <span>hits/hour (default 30)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Minimum required hits per hour for a domain name to be eligible for auto prefetch.</div>\n                                                        </div>\n\n                                                        <div>The DNS Server cache auto prefetch option can keep eligible domain names from last hour stats \"hot\" in cache. Auto prefetch eligibility value can be decided by keeping an eye on the hits shown for last hour on the dashboard. Experiment with auto prefetch sampling interval and eligibility to get best results.</div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneBlocking\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Blocking</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableBlocking\" type=\"checkbox\"> Enable Blocking\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Sets the DNS server to block domain names using Blocked Zone and Block List Zone.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkAllowTxtBlockingReport\" type=\"checkbox\"> Allow TXT Blocking Report\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. This option also enables Extended DNS Error blocked domain report in response for requests that support EDNS.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Blocking Temporarily Disabled Till</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div id=\"lblTemporaryDisableBlockingTill\" style=\"padding-top: 7px;\"></div>\n                                                                <div style=\"padding-top: 12px;\">\n                                                                    <input type=\"number\" class=\"form-control\" id=\"txtTemporaryDisableBlockingMinutes\" placeholder=\"minutes\" style=\"width: 100px; display: inline;\">\n                                                                    <span>minutes</span>\n                                                                </div>\n                                                                <div style=\"padding-top: 6px;\">\n                                                                    <button id=\"btnTemporaryDisableBlockingNow\" type=\"button\" class=\"btn btn-default\" style=\"padding: 2px 0; width: 170px;\" data-loading-text=\"Disabling...\" onclick=\"temporaryDisableBlockingNow();\">Temporary Disable Now</button>\n                                                                </div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtBlockingBypassList\" class=\"col-sm-3 control-label\">Blocking Bypass List</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtBlockingBypassList\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another that are allowed to bypass blocking.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Blocking Type</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdBlockingType\" id=\"rdBlockingTypeAnyAddress\" value=\"AnyAddress\">\n                                                                        ANY Address\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Uses <code>0.0.0.0</code> and <code>::</code> IP addresses for blocked domain names.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdBlockingType\" id=\"rdBlockingTypeNxDomain\" value=\"NxDomain\">\n                                                                        NX Domain (recommended)\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Uses <code>NX Domain</code> response for blocked domain names.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdBlockingType\" id=\"rdBlockingTypeCustomAddress\" value=\"CustomAddress\">\n                                                                        Custom Address\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Uses custom IP addresses provided below for blocked domain names.</div>\n                                                                </div>\n                                                            </div>\n\n                                                            <div class=\"col-sm-offset-3 col-sm-6\" style=\"margin-bottom: 10px;\">\n                                                                <label for=\"txtCustomBlockingAddresses\" class=\"control-label\">Custom Blocking Addresses (IP Address)</label>\n                                                                <textarea id=\"txtCustomBlockingAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtBlockingAnswerTtl\" class=\"col-sm-3 control-label\">Blocking Answer TTL</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtBlockingAnswerTtl\" placeholder=\"ttl\" style=\"width: 100px; display: inline;\">\n                                                                <span>seconds (default 30)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtBlockListUrls\" class=\"col-sm-3 control-label\">Allow / Block List URLs</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtBlockListUrls\" class=\"form-control\" rows=\"7\" spellcheck=\"false\"></textarea>\n\n                                                                <label for=\"optQuickBlockList\" class=\"control-label\">Quick Add</label>\n                                                                <select id=\"optQuickBlockList\" class=\"form-control\" style=\"width: 100%;\">\n                                                                </select>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">\n                                                                <p>Enter block list URL one below another in the above text field or use the Quick Add list to add known block list URLs.</p>\n                                                                <p>For directly using block list files saved on this server, use the <code>file://</code> formatted URL path. For example, on Linux the URL should look like <code>file:///home/folder/myblocklist.txt</code> and on Windows it should look like <code>file:///c:/folder/myblocklist.txt</code>.</p>\n                                                                <p>Add <code>!</code> character at the start of an URL to make it an allow list URL. This option must not be used with allow lists that use <code>Adblock Plus</code> format.</p>\n                                                                <p>Begin a line with <code>#</code> character at the start to use it for comments.</p>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtBlockListUpdateIntervalHours\" class=\"col-sm-3 control-label\">Block List Update Interval</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtBlockListUpdateIntervalHours\" placeholder=\"hours\" style=\"width: 100px; display: inline;\">\n                                                                <span>hours (valid range 0-168; default 24; set 0 to disable)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The interval in hours to automatically download and update the block lists.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"lblBlockListNextUpdatedOn\" class=\"col-sm-3 control-label\">Block List Next Update On</label>\n                                                            <div class=\"col-sm-6\" style=\"padding-top: 5px;\">\n                                                                <span id=\"lblBlockListNextUpdatedOn\" style=\"margin-right: 15px;\"></span>\n                                                                <button id=\"btnUpdateBlockListsNow\" type=\"button\" class=\"btn btn-default\" style=\"padding: 2px 0; width: 100px;\" data-loading-text=\"Updating...\" onclick=\"forceUpdateBlockLists();\">Update Now</button>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Click the 'Update Now' button to reset the next update schedule and force download and update of the block lists.</div>\n                                                        </div>\n\n                                                        <div style=\"margin-top: 10px;\">Note! DNS Server will use the data returned by the block list URLs to update the block list zone automatically. The expected file format is standard <code>hosts</code> file format, plain text file containing list of domains to block, wildcard block list file format, or <code>Adblock Plus</code> file format.</div>\n                                                        <div style=\"margin-top: 10px;\">Note! To customize the Quick Add drop down list, read the instructions given in the <code>www/json/readme.txt</code> file found in the installation folder.</div>\n                                                        <div style=\"margin-top: 10px;\"><a href=\"https://blog.technitium.com/2018/10/blocking-internet-ads-using-dns-sinkhole.html\" target=\"_blank\">Help: Blocking Internet Ads Using DNS Sinkhole</a></div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneProxyForwarders\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Network Proxy</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdProxyType\" id=\"rdProxyTypeNone\" value=\"None\" checked>\n                                                                        No Proxy (default)\n                                                                    </label>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdProxyType\" id=\"rdProxyTypeHttp\" value=\"Http\">\n                                                                        HTTP Proxy\n                                                                    </label>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdProxyType\" id=\"rdProxyTypeSocks5\" value=\"Socks5\">\n                                                                        SOCKS5 Proxy\n                                                                    </label>\n                                                                </div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtProxyAddress\" class=\"col-sm-3 control-label\">Proxy Server Address</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtProxyAddress\" placeholder=\"domain name or IP address\" maxlength=\"255\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtProxyPort\" class=\"col-sm-3 control-label\">Proxy Server Port</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtProxyPort\" placeholder=\"port\" style=\"width: 100px;\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtProxyUsername\" class=\"col-sm-3 control-label\">Username</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtProxyUsername\" placeholder=\"username\" maxlength=\"255\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtProxyPassword\" class=\"col-sm-3 control-label\">Password</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"password\" class=\"form-control\" id=\"txtProxyPassword\" placeholder=\"password\" maxlength=\"255\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtProxyBypassList\" class=\"col-sm-3 control-label\">Proxy Bypass List</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtProxyBypassList\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Enter IP addresses, network addresses or domain names to never proxy.</div>\n                                                        </div>\n\n                                                        <div style=\"margin-top: 10px;\">Note! When proxy server is configured, DNS Server will use it for all outbound network requests.</div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtForwarders\" class=\"col-sm-3 control-label\">Forwarders</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <textarea id=\"txtForwarders\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n\n                                                                <label for=\"optQuickForwarders\" class=\"control-label\">Quick Select</label>\n                                                                <select id=\"optQuickForwarders\" class=\"form-control\" style=\"width: 100%;\">\n                                                                </select>\n\n                                                                <div style=\"padding-top: 5px;\">Enter forwarder DNS Server IP addresses or URLs one below another in above text field or use the Quick Select list to select desired forwarder.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Forwarder Protocol</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdForwarderProtocol\" id=\"rdForwarderProtocolUdp\" value=\"Udp\" checked>\n                                                                        DNS-over-UDP (default)\n                                                                    </label>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdForwarderProtocol\" id=\"rdForwarderProtocolTcp\" value=\"Tcp\">\n                                                                        DNS-over-TCP\n                                                                    </label>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdForwarderProtocol\" id=\"rdForwarderProtocolTls\" value=\"Tls\">\n                                                                        DNS-over-TLS\n                                                                    </label>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdForwarderProtocol\" id=\"rdForwarderProtocolHttps\" value=\"Https\">\n                                                                        DNS-over-HTTPS\n                                                                    </label>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdForwarderProtocol\" id=\"rdForwarderProtocolQuic\" value=\"Quic\">\n                                                                        DNS-over-QUIC\n                                                                    </label>\n                                                                </div>\n\n                                                                <div style=\"padding-top: 5px;\">Select a protocol that this DNS server must use to query the forwarders specified above.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Concurrent Forwarding</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableConcurrentForwarding\" type=\"checkbox\"> Enable Concurrent Forwarding\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option 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.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtForwarderConcurrency\" class=\"col-sm-3 control-label\">Forwarder Concurrency</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtForwarderConcurrency\" placeholder=\"count\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 1-10; default 2)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The number of concurrent requests that must be sent when Concurrent Forwarding is enabled for resolving a domain name.</div>\n                                                        </div>\n\n                                                        <div style=\"margin-top: 10px;\">Note! Forwarders are upstream DNS servers which this DNS Server must use to resolve domain names. If no forwarders are configured then the DNS server will use preconfigured ROOT HINTS to perform recursive resolution to resolve domain names.</div>\n                                                        <div style=\"margin-top: 10px;\">Note! The <code>https</code> URL scheme supports only DNS-over-HTTPS/2 and DNS-over-HTTPS/1.1 protocols. For DNS-over-HTTPS/3, use <code>h3</code> URL scheme instead of <code>https</code> but note that there wont be any protocol fallback if the connection attempt fails.</div>\n                                                        <div style=\"margin-top: 10px;\">Note! The DNS server uses Epsilon-Greedy machine learning algorithm and will automatically learn which of the forwarders are answering faster without errors and will use those forwarders most of the time.</div>\n                                                        <div style=\"margin-top: 10px;\">Note! To customize the Quick Select drop down list, read the instructions given in the <code>www/json/readme.txt</code> file found in the installation folder.</div>\n                                                        <div style=\"margin-top: 10px;\"><a href=\"https://blog.technitium.com/2018/06/configuring-dns-server-for-privacy.html\" target=\"_blank\">Help: Configuring DNS Server For Privacy & Security</a></div>\n                                                        <div style=\"margin-top: 10px;\"><a href=\"https://blog.technitium.com/2023/02/configuring-dns-over-quic-and-https3.html\" target=\"_blank\">Help: Configuring DNS-over-QUIC and HTTPS/3 For Technitium DNS Server</a></div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtForwarderRetries\" class=\"col-sm-3 control-label\">Forwarder Retries</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtForwarderRetries\" placeholder=\"retries\" style=\"width: 100px; display: inline;\">\n                                                                <span>(valid range 1-10; default 3)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The total number of retries the forwarder or conditional forwarder resolver must do per upstream DNS server.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtForwarderTimeout\" class=\"col-sm-3 control-label\">Forwarder Timeout</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtForwarderTimeout\" placeholder=\"timeout\" style=\"width: 100px; display: inline;\">\n                                                                <span>milliseconds (valid range 1000-10000; default 2000)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The amount of time the forwarder or conditional forwarder resolver must wait between retries.</div>\n                                                        </div>\n                                                    </div>\n                                                </div>\n\n                                                <div id=\"settingsTabPaneLogging\" role=\"tabpanel\" class=\"tab-pane\">\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Enable Logging To</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdLoggingType\" id=\"rdLoggingTypeNone\" value=\"None\">\n                                                                        None\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Disables all logging including error logs and audit logs.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdLoggingType\" id=\"rdLoggingTypeFile\" value=\"File\">\n                                                                        File\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enables logging errors and audit logs to the log file.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdLoggingType\" id=\"rdLoggingTypeConsole\" value=\"Console\">\n                                                                        Console\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enables logging errors and audit logs to the console.</div>\n                                                                </div>\n                                                                <div class=\"radio\">\n                                                                    <label>\n                                                                        <input type=\"radio\" name=\"rdLoggingType\" id=\"rdLoggingTypeFileAndConsole\" value=\"FileAndConsole\">\n                                                                        Both File And Console\n                                                                    </label>\n                                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enables logging errors and audit logs to both the log file and console.</div>\n                                                                </div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Logging Options</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkIgnoreResolverLogs\" type=\"checkbox\"> Ignore Resolver Error Logs\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to stop logging domain name resolution errors into the log file.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkLogQueries\" type=\"checkbox\"> Log All Queries\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to log every query received by this DNS Server and the corresponding response answers into the log file.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkUseLocalTime\" type=\"checkbox\"> Use Local Time\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to use local time instead of UTC for logging.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtLogFolderPath\" class=\"col-sm-3 control-label\">Log Folder Path</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtLogFolderPath\" placeholder=\"Log Folder Path On Server\" maxlength=\"255\">\n                                                            </div>\n\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The folder path on the server where the log files should be saved. The path can be relative to the DNS server's config folder.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtMaxLogFileDays\" class=\"col-sm-3 control-label\">Max Log File Days</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtMaxLogFileDays\" placeholder=\"Max Days\" style=\"width: 100px; display: inline;\">\n                                                                <span>days (default 365, set 0 to disable auto delete)</span>\n                                                            </div>\n\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Max number of days to keep the log files. Log files older than the specified number of days will be deleted automatically.</div>\n                                                        </div>\n\n                                                        <div>Warning! Enabling query logging will significantly increase the log file size and use up disk space.</div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label class=\"col-sm-3 control-label\">Stats</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkEnableInMemoryStats\" type=\"checkbox\"> Enable In-Memory Stats\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">This option will enable in-memory stats and only Last Hour data will be available on Dashboard. No stats data will be stored on disk.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtMaxStatFileDays\" class=\"col-sm-3 control-label\">Max Stat File Days</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"number\" class=\"form-control\" id=\"txtMaxStatFileDays\" placeholder=\"Max Days\" style=\"width: 100px; display: inline;\">\n                                                                <span>days (default 365, set 0 to disable auto delete)</span>\n                                                            </div>\n\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">Max number of days to keep the dashboard stats. Stat files older than the specified number of days will be deleted automatically.</div>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            </div>\n\n                                            <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                                <div class=\"pull-left\">\n                                                    <button type=\"button\" class=\"btn btn-primary\" data-loading-text=\"Saving...\" onclick=\"saveDnsSettings(this);\">Save Settings</button>\n                                                    <button id=\"btnSettingsFlushCache\" type=\"button\" class=\"btn btn-danger\" data-loading-text=\"Flushing...\" onclick=\"flushDnsCache(this, $('#optSettingsClusterNode').val());\" style=\"margin-left: 6px;\">Flush Cache</button>\n                                                </div>\n                                                <div class=\"pull-right\">\n                                                    <button id=\"btnShowBackupSettingsModal\" type=\"button\" class=\"btn btn-success\" onclick=\"resetBackupSettingsModal();\" data-toggle=\"modal\" data-target=\"#modalBackupSettings\">Backup Settings</button>\n                                                    <button id=\"btnShowRestoreSettingsModal\" type=\"button\" class=\"btn btn-warning\" onclick=\"resetRestoreSettingsModal();\" data-toggle=\"modal\" data-target=\"#modalRestoreSettings\" style=\"margin-left: 6px;\">Restore Settings</button>\n                                                </div>\n                                                <div class=\"clearfix\"></div>\n                                            </div>\n\n                                        </form>\n                                    </div>\n\n                                </div>\n\n                                <div id=\"mainPanelTabPaneDhcp\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n\n                                    <ul class=\"nav nav-tabs\" role=\"tablist\">\n                                        <li id=\"dhcpTabListLeases\" role=\"presentation\" class=\"active\"><a href=\"#dhcpTabPaneLeases\" aria-controls=\"dhcpTabPaneLeases\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshDhcpLeases();\">Leases</a></li>\n                                        <li id=\"dhcpTabListScopes\" role=\"presentation\"><a href=\"#dhcpTabPaneScopes\" aria-controls=\"dhcpTabPaneScopes\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshDhcpScopes(true);\">Scopes</a></li>\n\n                                        <li class=\"pull-right\">\n                                            <select id=\"optDhcpClusterNode\" class=\"form-control pull-right cluster-node-dropdown\" style=\"margin-left: 0px;\" onchange=\"refreshDhcpTab();\"></select>\n                                        </li>\n                                    </ul>\n\n                                    <div class=\"tab-content\">\n                                        <div id=\"dhcpTabPaneLeases\" class=\"tab-pane active\">\n                                            <div id=\"divDhcpLeasesLoader\" style=\"margin-top: 10px; height: 350px;\"></div>\n\n                                            <div id=\"divDhcpLeases\" style=\"margin-top: 10px;\">\n                                                <table class=\"table table-hover\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpLeasesBody', 0); return false;\">Scope</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpLeasesBody', 1); return false;\">MAC Address</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpLeasesBody', 2); return false;\">IP Address</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpLeasesBody', 3); return false;\"></a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpLeasesBody', 4); return false;\">Host Name</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpLeasesBody', 5); return false;\">Lease Obtained</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpLeasesBody', 6); return false;\">Lease Expires</a></th>\n                                                            <th style=\"width: 36px;\"></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tableDhcpLeasesBody\">\n                                                    </tbody>\n                                                    <tfoot id=\"tableDhcpLeasesFooter\">\n                                                        <tr><td><b>Total Leases: 0</b></td></tr>\n                                                    </tfoot>\n                                                </table>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"dhcpTabPaneScopes\" class=\"tab-pane\">\n                                            <div id=\"divDhcpViewScopesLoader\" style=\"margin-top: 10px; height: 350px;\"></div>\n\n                                            <div id=\"divDhcpViewScopes\" style=\"margin-top: 10px;\">\n\n                                                <div style=\"float: right; padding: 2px 0px;\">\n                                                    <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showAddDhcpScope();\">Add Scope</button>\n                                                </div>\n                                                <div style=\"clear: both;\"></div>\n\n                                                <table class=\"table table-hover\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpScopesBody', 0); return false;\">Name</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpScopesBody', 1); return false;\">Scope Range/Subnet Mask</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpScopesBody', 2); return false;\">Network/Broadcast</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tableDhcpScopesBody', 3); return false;\">Interface</a></th>\n                                                            <th></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tableDhcpScopesBody\">\n                                                    </tbody>\n                                                    <tfoot id=\"tableDhcpScopesFooter\">\n                                                        <tr><td><b>Total Leases: 0</b></td></tr>\n                                                    </tfoot>\n                                                </table>\n\n                                            </div>\n\n                                            <div id=\"divDhcpEditScope\" style=\"display: none;\">\n                                                <form style=\"margin-top: 10px; margin-bottom: 0px;\" onsubmit=\"return false;\">\n\n                                                    <h4 style=\"padding: 10px 0px;\" id=\"titleDhcpEditScope\">Edit Scope</h4>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeName\" class=\"col-sm-3 control-label\">Name</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeName\" data-name=\"\" placeholder=\"Scope Name\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeStartingAddress\" class=\"col-sm-3 control-label\">Starting Address</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeStartingAddress\" placeholder=\"Starting Address\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeEndingAddress\" class=\"col-sm-3 control-label\">Ending Address</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeEndingAddress\" placeholder=\"Ending Address\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeSubnetMask\" class=\"col-sm-3 control-label\">Subnet Mask</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeSubnetMask\" placeholder=\"Subnet Mask\">\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeLeaseTimeDays\" class=\"col-sm-3 control-label\">Lease Time</label>\n                                                            <div class=\"col-sm-7\">\n                                                                <label for=\"txtDhcpScopeLeaseTimeDays\" class=\"control-label\">Days</label>\n                                                                <input type=\"number\" class=\"form-control\" style=\"display: inline; width: 80px; margin-right: 15px;\" id=\"txtDhcpScopeLeaseTimeDays\" placeholder=\"Days\">\n\n                                                                <label for=\"txtDhcpScopeLeaseTimeHours\" class=\"control-label\">Hours</label>\n                                                                <input type=\"number\" class=\"form-control\" style=\"display: inline; width: 80px; margin-right: 15px;\" id=\"txtDhcpScopeLeaseTimeHours\" placeholder=\"Hrs\">\n\n                                                                <label for=\"txtDhcpScopeLeaseTimeMinutes\" class=\"control-label\">Minutes</label>\n                                                                <input type=\"number\" class=\"form-control\" style=\"display: inline; width: 80px; margin-right: 15px;\" id=\"txtDhcpScopeLeaseTimeMinutes\" placeholder=\"Mins\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The duration for which the clients should be leased the IP address.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                                            <label for=\"txtDhcpScopeOfferDelayTime\" class=\"col-sm-3 control-label\">Offer Delay Time</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"number\" class=\"form-control\" style=\"width: 80px; display: inline;\" id=\"txtDhcpScopeOfferDelayTime\" placeholder=\"Delay\">\n                                                                <span>milliseconds</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The time duration that the DHCP server delays sending an DHCPOFFER message.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"chkDhcpScopePingCheckEnabled\" class=\"col-sm-3 control-label\">Ping Check</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkDhcpScopePingCheckEnabled\" type=\"checkbox\"> Enable Ping Check\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to allow 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.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopePingCheckTimeout\" class=\"col-sm-3 control-label\">Ping Check Timeout</label>\n                                                            <div class=\"col-sm-4\">\n                                                                <input type=\"number\" class=\"form-control\" style=\"width: 100px; display: inline;\" id=\"txtDhcpScopePingCheckTimeout\" placeholder=\"timeout\">\n                                                                <span>milliseconds (default 1000)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The timeout interval to wait for an ping reply.</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopePingCheckRetries\" class=\"col-sm-3 control-label\">Ping Check Retries</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"number\" class=\"form-control\" style=\"width: 100px; display: inline;\" id=\"txtDhcpScopePingCheckRetries\" placeholder=\"retry\">\n                                                                <span>(default 2)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The maximum number of ping requests to try.</div>\n                                                        </div>\n\n                                                        <div style=\"padding-top: 5px;\">Warning! Ping check would work as expected only when you make sure that all the client devices with manually configured IP addresses on the network respond to a ping request. Devices running Microsoft Windows by default drop ping requests at host firewall and will cause this ping check to fail to detect in use IP addresses. It is recommended to not rely on this option and instead make sure that you exclude a range of addresses using Exclusions and manually assign IP addresses to your devices only in the excluded range.</div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeDomainName\" class=\"col-sm-3 control-label\">Domain Name</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeDomainName\" placeholder=\"Domain Name\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The domain name for this network to allow assigning a fully qualified domain name to clients. Use a domain name that you own or that is not in common use like 'home' or 'lan' so that you don't block out an existing domain name. (Option 15)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeDomainSearchStrings\" class=\"col-sm-3 control-label\">Domain Search List</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <textarea id=\"txtDhcpScopeDomainSearchStrings\" class=\"form-control\" rows=\"2\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The list of domain names that the clients can use as a suffix when searching a domain name. (Option 119)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"chkDhcpScopeDnsUpdates\" class=\"col-sm-3 control-label\">DNS Updates</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkDhcpScopeDnsUpdates\" type=\"checkbox\"> Enable DNS Updates\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to allow the DHCP server to automatically update forward and reverse DNS entries for clients.</div>\n\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkDnsOverwriteForDynamicLease\" type=\"checkbox\"> Enable DNS Overwrite For Dynamic Lease\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to allow the DHCP server to overwrite existing DNS A record matching the client domain name for dynamic leases.</div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeDnsTtl\" class=\"col-sm-3 control-label\">DNS TTL</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <input type=\"text\" class=\"form-control\" style=\"width: 100px; display: inline;\" id=\"txtDhcpScopeDnsTtl\" placeholder=\"DNS TTL\">\n                                                                <span>seconds (default 900/15m)</span>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The TTL value of the DNS records updated for the above provided domain name.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeRouterAddress\" class=\"col-sm-3 control-label\">Router Address</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeRouterAddress\" placeholder=\"Router Address\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The default gateway IP address to be used by the clients. (Option 3)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeDnsServers\" class=\"col-sm-3 control-label\">DNS Servers</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkUseThisDnsServer\" type=\"checkbox\" onclick=\"$('#txtDhcpScopeDnsServers').prop('disabled', $(this).prop('checked'));\"> Use This DNS Server\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px; margin-bottom: 10px;\">Enable this option to automatically use this DNS Server.</div>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-3\">\n                                                                <textarea id=\"txtDhcpScopeDnsServers\" class=\"form-control\" rows=\"2\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The DNS server IP addresses to be used by the clients. (Option 6)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeWinsServers\" class=\"col-sm-3 control-label\">WINS Servers</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <textarea id=\"txtDhcpScopeWinsServers\" class=\"form-control\" rows=\"2\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The NBNS/WINS server IP addresses to be used by the clients. (Option 44)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeNtpServers\" class=\"col-sm-3 control-label\">NTP Servers</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <textarea id=\"txtDhcpScopeNtpServers\" class=\"form-control\" rows=\"2\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The Network Time Protocol (NTP) server IP addresses to be used by the clients. (Option 42)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeNtpServerDomainNames\" class=\"col-sm-3 control-label\">NTP Server Domain Names</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <textarea id=\"txtDhcpScopeNtpServerDomainNames\" class=\"form-control\" rows=\"2\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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)</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                                            <label for=\"tableDhcpScopeStaticRoutes\" class=\"col-sm-3 control-label\">Static Routes</label>\n                                                            <div class=\"col-sm-7\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th>Destination</th>\n                                                                            <th>Subnet Mask</th>\n                                                                            <th>Router</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addDhcpScopeStaticRouteRow('', '', '');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableDhcpScopeStaticRoutes\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The static routes to be used by the clients for accessing specified destination networks. (Option 121)</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeServerAddress\" class=\"col-sm-3 control-label\">Bootstrap Server Address</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeServerAddress\" placeholder=\"Bootstrap Server Address\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">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)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeServerHostName\" class=\"col-sm-3 control-label\">Bootstrap Server Host Name</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeServerHostName\" placeholder=\"Bootstrap Server Host Name\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The optional bootstrap server host name to be used by the clients to identify the TFTP server. (sname/Option 66)</div>\n                                                        </div>\n\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeBootFileName\" class=\"col-sm-3 control-label\">Boot File Name</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <input type=\"text\" class=\"form-control\" id=\"txtDhcpScopeBootFileName\" placeholder=\"Boot File Name\">\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The boot file name stored on the bootstrap TFTP server to be used by the clients. (file/Option 67)</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                                            <label for=\"tableDhcpScopeVendorInfo\" class=\"col-sm-3 control-label\">Vendor Specific Information</label>\n                                                            <div class=\"col-sm-9\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th>Vendor Class Identifier</th>\n                                                                            <th>Vendor Specific Information</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addDhcpScopeVendorInfoRow('', '');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableDhcpScopeVendorInfo\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The Vendor Specific Information (option 43) to be sent to the clients that match the Vendor Class Identifier (option 60) in the request. The Vendor Class Identifier can be empty string to match any identifier, or matched exactly, or match a substring, for example <code>substring(vendor-class-identifier,0,9)==\"PXEClient\"</code>. The Vendor Specific Information must be either a colon (:) separated hex string or a normal hex string, for example <code>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</code> OR <code>0601030A0400505845091400001152617370626572727920506920426F6F74FF</code>.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeCAPWAPApIpAddresses\" class=\"col-sm-3 control-label\">CAPWAP Access Controller Addresses</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <textarea id=\"txtDhcpScopeCAPWAPApIpAddresses\" class=\"form-control\" rows=\"2\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The 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)</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"txtDhcpScopeTftpServerAddresses\" class=\"col-sm-3 control-label\">TFTP Server Addresses</label>\n                                                            <div class=\"col-sm-3\">\n                                                                <textarea id=\"txtDhcpScopeTftpServerAddresses\" class=\"form-control\" rows=\"2\" spellcheck=\"false\"></textarea>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The TFTP Server Address or the VoIP Configuration Server Address. (Option 150)</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                                            <label for=\"tableDhcpScopeGenericOptions\" class=\"col-sm-3 control-label\">Generic DHCP Options</label>\n                                                            <div class=\"col-sm-9\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th style=\"width: 90px;\">Code</th>\n                                                                            <th>Hex Value</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addDhcpScopeGenericOptionsRow('', '');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableDhcpScopeGenericOptions\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">This feature allows you to define DHCP options that are not yet directly supported. To add an option, use the DHCP option code defined for it and enter the value in either a colon (:) separated hex string or a normal hex string format, for example <code>C0:A8:01:01</code> OR <code>C0A80101</code>.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"tableDhcpScopeExclusions\" class=\"col-sm-3 control-label\">Exclusions</label>\n                                                            <div class=\"col-sm-6\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th>Starting Address</th>\n                                                                            <th>Ending Address</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addDhcpScopeExclusionRow('', '');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableDhcpScopeExclusions\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The IP address range that must be excluded or not assigned dynamically to any client by the DHCP server.</div>\n                                                        </div>\n                                                        <div style=\"padding-top: 5px;\">Note! Make sure to exclude address ranges if you plan to manually assign IP addresses to some of the devices or to assign reserved leases so that these IP addresses are not dynamically allocated in the first place.</div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\">\n                                                            <label for=\"tableDhcpScopeReservedLeases\" class=\"col-sm-3 control-label\">Advanced Options</label>\n                                                            <div class=\"col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkAllowOnlyReservedLeases\" type=\"checkbox\"> Allow Only Reserved Lease Allocations\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to stop dynamic IP address allocation and allocate only reserved IP addresses.</div>\n                                                            </div>\n\n                                                            <div class=\"col-sm-offset-3 col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkBlockLocallyAdministeredMacAddresses\" type=\"checkbox\"> Block Locally Administered MAC Addresses\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option 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 <a href=\"https://en.wikipedia.org/wiki/MAC_address\" target=\"_blank\">locally administered</a> MAC address which usually means that the device is not using its original MAC address.</div>\n                                                            </div>\n\n                                                            <div class=\"col-sm-offset-3 col-sm-8\">\n                                                                <div class=\"checkbox\">\n                                                                    <label>\n                                                                        <input id=\"chkIgnoreClientIdentifierOption\" type=\"checkbox\"> Ignore Client Identifier (Option 61)\n                                                                    </label>\n                                                                </div>\n                                                                <div style=\"padding-top: 5px; padding-left: 20px;\">This option when enabled will 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. Some Linux distros use a custom Client Identifier instead of the device's MAC Address which can cause issues when the Virtual Machine (VM) in which the OS is installed is cloned causing both the original and cloned clients to get same IP allocated. There can be issues too when the same client changes its Client Identifier and starts getting a different IP address lease. Enabling the Ignore Client Identifier option will fix such issues. Changing this option may cause the existing clients to get a different IP lease on renewal.</div>\n                                                            </div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"well well-sm form-horizontal\">\n                                                        <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                                            <label for=\"tableDhcpScopeReservedLeases\" class=\"col-sm-3 control-label\">Reserved Leases</label>\n                                                            <div class=\"col-sm-9\">\n                                                                <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                                                    <thead>\n                                                                        <tr>\n                                                                            <th>Host Name</th>\n                                                                            <th>MAC Address</th>\n                                                                            <th>IP Address</th>\n                                                                            <th>Comments</th>\n                                                                            <th style=\"width: 84px;\"><button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addDhcpScopeReservedLeaseRow('', '', '', '');\">Add</button></th>\n                                                                        </tr>\n                                                                    </thead>\n                                                                    <tbody id=\"tableDhcpScopeReservedLeases\"></tbody>\n                                                                </table>\n                                                            </div>\n                                                            <div class=\"col-sm-offset-3 col-sm-8\" style=\"padding-top: 5px;\">The reserved IP addresses to be assigned to specific clients based on their MAC address. Set a hostname to override the client's hostname.</div>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"form-group\" style=\"margin-bottom: 0px;\">\n                                                        <button type=\"submit\" class=\"btn btn-primary\" style=\"width: 100px;\" id=\"btnSaveDhcpScope\" data-loading-text=\"Saving...\" onclick=\"saveDhcpScope(); return false;\">Save</button>\n                                                        <button type=\"button\" class=\"btn btn-default\" style=\"width: 100px;\" onclick=\"refreshDhcpScopes();\">Cancel</button>\n                                                    </div>\n                                                </form>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div id=\"mainPanelTabPaneAdmin\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n\n                                    <div>\n                                        <ul class=\"nav nav-tabs\" role=\"tablist\">\n                                            <li id=\"adminTabListSessions\" role=\"presentation\" class=\"active\"><a href=\"#adminTabPaneSessions\" aria-controls=\"adminTabPaneSessions\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshAdminSessions();\">Sessions</a></li>\n                                            <li id=\"adminTabListUsers\" role=\"presentation\"><a href=\"#adminTabPaneUsers\" aria-controls=\"adminTabPaneUsers\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshAdminUsers();\">Users</a></li>\n                                            <li id=\"adminTabListGroups\" role=\"presentation\"><a href=\"#adminTabPaneGroups\" aria-controls=\"adminTabPaneGroups\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshAdminGroups();\">Groups</a></li>\n                                            <li id=\"adminTabListPermissions\" role=\"presentation\"><a href=\"#adminTabPanePermissions\" aria-controls=\"adminTabPanePermissions\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshAdminPermissions();\">Permissions</a></li>\n                                            <li id=\"adminTabListCluster\" role=\"presentation\"><a href=\"#adminTabPaneCluster\" aria-controls=\"adminTabPaneCluster\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshAdminCluster();\">Cluster</a></li>\n                                        </ul>\n\n                                        <div class=\"clearfix\"></div>\n                                    </div>\n\n                                    <div class=\"tab-content\">\n\n                                        <div id=\"adminTabPaneSessions\" class=\"tab-pane active\">\n                                            <div id=\"divAdminSessionsLoader\" style=\"margin-top: 10px; height: 350px;\"></div>\n\n                                            <div id=\"divAdminSessionsView\" style=\"margin-top: 10px;\">\n                                                <div class=\"form-inline pull-right\" style=\"padding: 2px 0px;\">\n                                                    <button id=\"btnAdminSessionsCreateToken\" type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showCreateApiTokenModal();\">Create Token</button>\n                                                    <select id=\"optAdminSessionsClusterNode\" class=\"form-control cluster-node-dropdown\" onchange=\"refreshAdminSessions();\"></select>\n                                                </div>\n\n                                                <div style=\"clear: both;\"></div>\n\n                                                <table id=\"tableAdminSessions\" class=\"table table-hover\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminSessions', 0); return false;\">Username</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminSessions', 1); return false;\">Session</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminSessions', 2); return false;\">Last Seen</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminSessions', 3); return false;\">Remote Address</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminSessions', 4); return false;\">User Agent</a></th>\n                                                            <th style=\"width: 36px;\"></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tbodyAdminSessions\">\n                                                    </tbody>\n                                                    <tfoot>\n                                                        <tr><th colspan=\"6\" id=\"tfootAdminSessions\"></th></tr>\n                                                    </tfoot>\n                                                </table>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"adminTabPaneUsers\" class=\"tab-pane\">\n                                            <div id=\"divAdminUsersLoader\" style=\"margin-top: 10px; height: 350px;\"></div>\n\n                                            <div id=\"divAdminUsersView\" style=\"margin-top: 10px;\">\n                                                <div style=\"float: right; padding: 2px 0px;\">\n                                                    <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showAddUserModal();\">Add User</button>\n                                                </div>\n\n                                                <div style=\"clear: both;\"></div>\n\n                                                <table id=\"tableAdminUsers\" class=\"table table-hover\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminUsers', 0); return false;\">Username</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminUsers', 1); return false;\">Display Name</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminUsers', 2); return false;\">2FA Status</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminUsers', 3); return false;\">Status</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminUsers', 4); return false;\">Recent Login</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminUsers', 5); return false;\">Previous Login</a></th>\n                                                            <th style=\"width: 36px;\"></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tbodyAdminUsers\">\n                                                    </tbody>\n                                                    <tfoot>\n                                                        <tr><th colspan=\"7\" id=\"tfootAdminUsers\"></th></tr>\n                                                    </tfoot>\n                                                </table>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"adminTabPaneGroups\" class=\"tab-pane\">\n                                            <div id=\"divAdminGroupsLoader\" style=\"margin-top: 10px; height: 350px;\"></div>\n\n                                            <div id=\"divAdminGroupsView\" style=\"margin-top: 10px;\">\n                                                <div style=\"float: right; padding: 2px 0px;\">\n                                                    <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showAddGroupModal();\">Add Group</button>\n                                                </div>\n\n                                                <div style=\"clear: both;\"></div>\n\n                                                <table id=\"tableAdminGroups\" class=\"table table-hover\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminGroups', 0); return false;\">Name</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminGroups', 1); return false;\">Description</a></th>\n                                                            <th style=\"width: 36px;\"></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tbodyAdminGroups\">\n                                                    </tbody>\n                                                    <tfoot>\n                                                        <tr><th colspan=\"3\" id=\"tfootAdminGroups\"></th></tr>\n                                                    </tfoot>\n                                                </table>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"adminTabPanePermissions\" class=\"tab-pane\">\n                                            <div id=\"divAdminPermissionsLoader\" style=\"margin-top: 10px; height: 350px;\"></div>\n\n                                            <div id=\"divAdminPermissionsView\" style=\"margin-top: 10px;\">\n                                                <table id=\"tableAdminPermissions\" class=\"table table-hover\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminPermissions', 0); return false;\">Section</a></th>\n                                                            <th>User Permissions</th>\n                                                            <th>Group Permissions</th>\n                                                            <th style=\"width: 36px;\"></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tbodyAdminPermissions\">\n                                                    </tbody>\n                                                    <tfoot>\n                                                        <tr><th colspan=\"4\" id=\"tfootAdminPermissions\"></th></tr>\n                                                    </tfoot>\n                                                </table>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"adminTabPaneCluster\" class=\"tab-pane\">\n                                            <div id=\"divAdminClusterLoader\" style=\"margin-top: 10px; height: 350px;\"></div>\n\n                                            <div id=\"divAdminClusterView\" style=\"margin-top: 10px;\">\n                                                <div class=\"form-inline pull-right\" style=\"padding: 2px 0px;\">\n                                                    <div id=\"divAdminClusterInitialize\" class=\"btn-group\">\n                                                        <button type=\"button\" class=\"btn btn-primary dropdown-toggle\" style=\"padding: 2px 0px; width: 100px;\" data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n                                                            Initialize <span class=\"caret\"></span>\n                                                        </button>\n                                                        <ul class=\"dropdown-menu\">\n                                                            <li id=\"lnkAdminClusterInitializeNewCluster\"><a href=\"#\" onclick=\"showInitializeClusterModal(); return false;\">New Cluster</a></li>\n                                                            <li id=\"lnkAdminClusterInitializeJoinCluster\"><a href=\"#\" onclick=\"showInitializeJoinClusterModal();  return false;\">Join Cluster</a></li>\n                                                        </ul>\n                                                    </div>\n\n                                                    <button id=\"btnClusterResync\" type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"resyncCluster(this);\" data-loading-text=\"Resyncing...\">Resync</button>\n                                                    <button id=\"btnClusterOptions\" type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showClusterOptionsModal();\">Options</button>\n                                                    <button id=\"btnClusterLeave\" type=\"button\" class=\"btn btn-warning\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showLeaveClusterModal();\">Leave Cluster</button>\n                                                    <button id=\"btnClusterDelete\" type=\"button\" class=\"btn btn-danger\" style=\"padding: 2px 0px; width: 100px;\" onclick=\"showDeleteClusterModal();\">Delete Cluster</button>\n                                                    <select id=\"optAdminClusterNode\" class=\"form-control cluster-node-dropdown\" onchange=\"refreshAdminCluster();\"></select>\n                                                </div>\n\n                                                <div style=\"clear: both;\"></div>\n\n                                                <table id=\"tableAdminCluster\" class=\"table table-hover\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 0); return false;\">Node Name</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 1); return false;\">IP Address</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 2); return false;\">URL</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 3); return false;\">Type</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 4); return false;\">State</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 5); return false;\">Up Since</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 6); return false;\">Last Seen</a></th>\n                                                            <th><a href=\"#\" onclick=\"sortTable('tbodyAdminCluster', 7); return false;\">Last Synced</a></th>\n                                                            <th style=\"width: 36px;\"></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tbodyAdminCluster\">\n                                                    </tbody>\n                                                    <tfoot>\n                                                        <tr><th colspan=\"9\" id=\"tfootAdminCluster\"></th></tr>\n                                                    </tfoot>\n                                                </table>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                </div>\n\n                                <div id=\"mainPanelTabPaneLogs\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 10px 0 0 0;\">\n\n                                    <ul class=\"nav nav-tabs\" role=\"tablist\">\n                                        <li id=\"logsTabListLogViewer\" role=\"presentation\" class=\"active\"><a href=\"#logsTabPaneLogViewer\" aria-controls=\"logsTabPaneLogViewer\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshLogFilesList();\">View Logs</a></li>\n                                        <li id=\"logsTabListQueryLogs\" role=\"presentation\"><a href=\"#logsTabPaneQueryLogs\" aria-controls=\"logsTabPaneQueryLogs\" role=\"tab\" data-toggle=\"tab\" onclick=\"refreshQueryLogsTab();\">Query Logs</a></li>\n\n                                        <li class=\"pull-right\">\n                                            <select id=\"optLogsClusterNode\" class=\"form-control pull-right cluster-node-dropdown\" style=\"margin-left: 0px;\" onchange=\"logsClusterNodeChanged();\"></select>\n                                        </li>\n                                    </ul>\n\n                                    <div class=\"tab-content\">\n                                        <div id=\"logsTabPaneLogViewer\" class=\"tab-pane active\" style=\"padding-top: 15px;\">\n\n                                            <div class=\"well well-sm log-list-pane\">\n                                                <div id=\"lstLogFiles\" class=\"logs\">\n                                                </div>\n                                            </div>\n\n                                            <div id=\"divLogViewer\" class=\"log-viewer-pane\">\n                                                <div class=\"panel panel-default\">\n                                                    <div class=\"panel-heading\" style=\"height: 36px; padding: 4px 6px;\">\n                                                        <div id=\"txtLogViewerTitle\" style=\"float: left; padding: 4px;\">20171012</div>\n                                                        <div style=\"float: right;\">\n                                                            <button type=\"button\" class=\"btn btn-default\" data-loading-text=\"Download\" onclick=\"downloadLog();\" style=\"font-size: 12px; padding: 4px 6px;\">Download</button>\n                                                            <button id=\"btnDeleteLog\" type=\"button\" class=\"btn btn-danger\" data-loading-text=\"Delete\" onclick=\"deleteLog();\" style=\"font-size: 12px; padding: 4px 6px;\">Delete</button>\n                                                        </div>\n                                                    </div>\n\n                                                    <div class=\"panel-body\">\n                                                        <div id=\"divLogViewerLoader\" style=\"margin-top: 20px; height: 400px;\"></div>\n                                                        <pre id=\"preLogViewerBody\" style=\"display: none; word-wrap: normal; word-break: normal;\"></pre>\n                                                    </div>\n                                                </div>\n                                            </div>\n\n                                        </div>\n\n                                        <div id=\"logsTabPaneQueryLogs\" class=\"tab-pane active\" style=\"padding-top: 15px;\">\n\n                                            <form id=\"frmQueryLogs\" class=\"form-inline well\" style=\"padding-bottom: 6px;\">\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogsAppName\">App Name</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogsAppName\">\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogsClassPath\">Class Path</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogsClassPath\">\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"txtQueryLogPageNumber\">Page Number</label>\n                                                    <input id=\"txtQueryLogPageNumber\" type=\"number\" class=\"form-control\" style=\"width: 120px;\" value=\"1\" />\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogsEntriesPerPage\">Logs Per Page</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogsEntriesPerPage\">\n                                                        <option>10</option>\n                                                        <option>25</option>\n                                                        <option>50</option>\n                                                        <option>100</option>\n                                                        <option>250</option>\n                                                        <option>500</option>\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogsDescendingOrder\">Order</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogsDescendingOrder\">\n                                                        <option value=\"false\">Ascending</option>\n                                                        <option value=\"true\" selected>Descending</option>\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"txtQueryLogStart\">From</label>\n                                                    <input id=\"txtQueryLogStart\" type=\"datetime-local\" class=\"form-control\" />\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"txtQueryLogEnd\">To</label>\n                                                    <input id=\"txtQueryLogEnd\" type=\"datetime-local\" class=\"form-control\" />\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"txtQueryLogClientIpAddress\">Client IP Address</label>\n                                                    <input id=\"txtQueryLogClientIpAddress\" type=\"text\" class=\"form-control\" style=\"min-width: 170px;\" />\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogsProtocol\">Protocol</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogsProtocol\">\n                                                        <option selected></option>\n                                                        <option value=\"Udp\">UDP</option>\n                                                        <option value=\"Tcp\">TCP</option>\n                                                        <option value=\"Tls\">TLS</option>\n                                                        <option value=\"Https\">HTTPS</option>\n                                                        <option value=\"Quic\">QUIC</option>\n                                                        <option value=\"UdpProxy\">UDP Proxy</option>\n                                                        <option value=\"TcpProxy\">TCP Proxy</option>\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogsResponseType\">Response Type</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogsResponseType\">\n                                                        <option selected></option>\n                                                        <option>Authoritative</option>\n                                                        <option>Recursive</option>\n                                                        <option>Cached</option>\n                                                        <option>Blocked</option>\n                                                        <option>UpstreamBlocked</option>\n                                                        <option>UpstreamBlockedCached</option>\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogsResponseCode\">RCODE</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogsResponseCode\">\n                                                        <option selected></option>\n                                                        <option value=\"NoError\">No Error</option>\n                                                        <option value=\"FormatError\">Format Error</option>\n                                                        <option value=\"ServerFailure\">Server Failure</option>\n                                                        <option value=\"NxDomain\">NX Domain</option>\n                                                        <option value=\"NotImplemented\">Not Implemented</option>\n                                                        <option value=\"Refused\">Refused</option>\n                                                        <option value=\"YXDomain\">YX Domain</option>\n                                                        <option value=\"YXRRSet\">YX RRSet</option>\n                                                        <option value=\"NXRRSet\">NX RRSet</option>\n                                                        <option value=\"NotAuth\">Not Auth</option>\n                                                        <option value=\"NotZone\">Not Zone</option>\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"txtQueryLogQName\">Domain</label>\n                                                    <input id=\"txtQueryLogQName\" type=\"text\" class=\"form-control\" style=\"min-width: 300px;\" />\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"txtQueryLogQType\">Type</label>\n                                                    <input id=\"txtQueryLogQType\" type=\"text\" class=\"form-control\" />\n                                                </div>\n\n                                                <div class=\"form-group\">\n                                                    <label for=\"optQueryLogQClass\">Class</label>\n                                                    <select class=\"form-control\" id=\"optQueryLogQClass\">\n                                                        <option selected></option>\n                                                        <option>IN</option>\n                                                        <option>CS</option>\n                                                        <option>CH</option>\n                                                        <option>HS</option>\n                                                        <option>NONE</option>\n                                                        <option>ANY</option>\n                                                    </select>\n                                                </div>\n\n                                                <div class=\"form-group\" style=\"display: block;\">\n                                                    <button type=\"submit\" class=\"btn btn-primary\" id=\"btnQueryLogs\" data-loading-text=\"Querying...\" onclick=\"queryLogs(); return false;\" style=\"margin-right: 6px;\">Query</button>\n                                                    <button type=\"button\" class=\"btn btn-default\" onclick=\"exportQueryLogsCsv(); return false;\" style=\"margin-right: 6px;\">Export</button>\n                                                    <button type=\"reset\" class=\"btn btn-default\">Reset</button>\n                                                </div>\n                                            </form>\n\n                                            <div id=\"divQueryLogsLoader\" style=\"margin-top: 20px; height: 300px;\"></div>\n\n                                            <div id=\"divQueryLogsTable\" style=\"display: none;\">\n                                                <div style=\"padding: 8px;\">\n                                                    <div class=\"pull-left\" style=\"padding-top: 8px;\">\n                                                        <b id=\"tableQueryLogsTopStatus\">Found: 0 logs</b>\n                                                    </div>\n                                                    <div class=\"pull-right\">\n                                                        <nav aria-label=\"Page navigation\">\n                                                            <ul id=\"tableQueryLogsTopPagination\" class=\"pagination\" style=\"margin: 0;\">\n                                                                <li><a href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">&laquo;</span></a></li>\n                                                                <li><a href=\"#\">1</a></li>\n                                                                <li><a href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">&raquo;</span></a></li>\n                                                            </ul>\n                                                        </nav>\n                                                    </div>\n                                                    <div class=\"clearfix\"></div>\n                                                </div>\n\n                                                <table class=\"table table-hover query-logs\">\n                                                    <thead>\n                                                        <tr>\n                                                            <th>#</th>\n                                                            <th style=\"width: 95px;\">Timestamp</th>\n                                                            <th>Client IP Address</th>\n                                                            <th>Protocol</th>\n                                                            <th>Response Type</th>\n                                                            <th>RCODE</th>\n                                                            <th style=\"min-width: 200px;\">Domain</th>\n                                                            <th>Type</th>\n                                                            <th>Class</th>\n                                                            <th>Answer</th>\n                                                            <th style=\"width: 36px;\"></th>\n                                                        </tr>\n                                                    </thead>\n                                                    <tbody id=\"tableQueryLogsBody\">\n                                                    </tbody>\n                                                    <tfoot>\n                                                        <tr>\n                                                            <td colspan=\"11\">\n                                                                <div>\n                                                                    <div class=\"pull-left\" style=\"padding-top: 8px;\">\n                                                                        <b id=\"tableQueryLogsFooterStatus\">Found: 0 logs</b>\n                                                                    </div>\n                                                                    <div class=\"pull-right\">\n                                                                        <nav aria-label=\"Page navigation\">\n                                                                            <ul id=\"tableQueryLogsFooterPagination\" class=\"pagination\" style=\"margin: 0;\">\n                                                                                <li><a href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">&laquo;</span></a></li>\n                                                                                <li><a href=\"#\">1</a></li>\n                                                                                <li><a href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">&raquo;</span></a></li>\n                                                                            </ul>\n                                                                        </nav>\n                                                                    </div>\n                                                                    <div class=\"clearfix\"></div>\n                                                                </div>\n                                                            </td>\n                                                        </tr>\n                                                    </tfoot>\n                                                </table>\n                                            </div>\n\n                                        </div>\n                                    </div>\n\n                                </div>\n\n                                <div id=\"mainPanelTabPaneAbout\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 40px 0 20px 0;\">\n\n                                    <div class=\"about\" style=\"text-align: center;\">\n                                        <img src=\"img/logo.png\" alt=\"Technitium Logo\" />\n                                        <h1>Technitium DNS Server</h1>\n                                        <p>Version <span id=\"lblAboutVersion\"></span></p>\n                                        <p>Server up since <span id=\"lblAboutUptime\"></span></p>\n                                        <p style=\"max-width: 800px; margin: 0 auto 10px auto;\">\n                                            Copyright (C) 2025  Shreyas Zare (shreyas@technitium.com)<br />\n                                            This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions.<br />\n                                        </p>\n                                        <p>Source code available under <a href=\"https://go.technitium.com/?id=24\" target=\"_blank\">GNU General Public License v3.0</a> on <a href=\"https://github.com/TechnitiumSoftware/DnsServer\" target=\"_blank\"><i class=\"fa fa-github\"></i>&nbsp;GitHub</a></p>\n\n                                        <h3 style=\"margin-top: 40px;\"><a href=\"https://go.technitium.com/?id=23\" target=\"_blank\">What's New?</a></h3>\n                                        <p>Read the <a href=\"https://go.technitium.com/?id=23\" target=\"_blank\">change log</a> to know what's new in this release.</p>\n\n                                        <h3 style=\"margin-top: 40px;\"><a href=\"https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md\" target=\"_blank\">API Documentation</a></h3>\n                                        <p>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 <a href=\"https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md\" target=\"_blank\">HTTP API documentation</a> for complete details.</p>\n\n                                        <h3 style=\"margin-top: 40px;\"><a href=\"https://go.technitium.com/?id=25\" target=\"_blank\">Help Topics</a></h3>\n                                        <p>Read the latest <a href=\"https://go.technitium.com/?id=25\" target=\"_blank\">online help topics</a> which contains the DNS Server user manual and covers frequently asked questions.</p>\n\n                                        <h3 style=\"margin-top: 40px;\">Support</h3>\n                                        <p>For support, send an email to <a href=\"mailto:support@technitium.com\" target=\"_blank\">support@technitium.com</a>.</p>\n                                        <p>\n                                            Follow <a href=\"https://mastodon.social/@technitium\" target=\"_blank\">@technitium@mastodon.social</a> on Mastodon.<br />\n                                            Checkout <a href=\"https://blog.technitium.com/\" target=\"_blank\">Technitium Blog</a>.\n                                        </p>\n                                        <p>\n                                            Join <a href=\"https://www.reddit.com/r/technitium/\" target=\"_blank\">/r/technitium</a> on Reddit.\n                                        </p>\n\n                                        <h3 style=\"margin-top: 40px;\"><a href=\"https://go.technitium.com/?id=35\" target=\"_blank\">Donate</a></h3>\n                                        <p>Make a contribution to Technitium and help making new software, updates, and features possible.</p>\n                                        <p>\n                                            <a href=\"https://go.technitium.com/?id=35\" target=\"_blank\">Donate Now!</a>\n                                        </p>\n                                    </div>\n\n                                </div>\n                            </div>\n\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n        </div>\n    </div>\n\n    <div id=\"modalMyProfile\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 940px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">My Profile</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divMyProfileAlert\"></div>\n\n                        <div id=\"divMyProfileLoader\" style=\"height: 500px;\"></div>\n\n                        <div id=\"divMyProfileViewer\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <div class=\"form-group\">\n                                <label for=\"txtMyProfileDisplayName\" class=\"col-sm-4 control-label\">Display Name</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtMyProfileDisplayName\" type=\"text\" class=\"form-control\" placeholder=\"display name\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtMyProfileUsername\" class=\"col-sm-4 control-label\">Username</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtMyProfileUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\" disabled>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"lblMyProfile2FAStatus\" class=\"col-sm-4 control-label\">2FA Status</label>\n                                <div class=\"col-sm-7\">\n                                    <div id=\"lblMyProfile2FAStatus\" style=\"padding: 6px 0; font-weight: bold;\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtMyProfileSessionTimeout\" class=\"col-sm-4 control-label\">Session Timeout</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtMyProfileSessionTimeout\" type=\"number\" class=\"form-control\" placeholder=\"1800\" style=\"width: 100px; display: inline;\">\n                                    <span>seconds (valid range 0-604800; default 1800; set 0 to disable)</span>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label class=\"col-sm-4 control-label\">Member Of</label>\n                                <div class=\"col-sm-7\">\n                                    <table class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                        <thead>\n                                            <tr>\n                                                <th><a href=\"#\" onclick=\"sortTable('tbodyMyProfileMemberOf', 0); return false;\">Group</a></th>\n                                            </tr>\n                                        </thead>\n                                        <tbody id=\"tbodyMyProfileMemberOf\">\n                                        </tbody>\n                                        <tfoot>\n                                            <tr><th colspan=\"1\" id=\"tfootMyProfileMemberOf\"></th></tr>\n                                        </tfoot>\n                                    </table>\n                                </div>\n                            </div>\n\n                            <div class=\"well well-sm\" style=\"background-color: #fbfbfb;\">\n                                <p style=\"font-size: 16px; font-weight: bold;\">Active Sessions</p>\n                                <table id=\"tableMyProfileActiveSessions\" class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                    <thead>\n                                        <tr>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyMyProfileActiveSessions', 0); return false;\">Session</a></th>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyMyProfileActiveSessions', 1); return false;\">Last Seen</a></th>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyMyProfileActiveSessions', 2); return false;\">Remote Address</a></th>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyMyProfileActiveSessions', 3); return false;\">User Agent</a></th>\n                                            <th style=\"width: 36px;\"></th>\n                                        </tr>\n                                    </thead>\n                                    <tbody id=\"tbodyMyProfileActiveSessions\">\n                                    </tbody>\n                                    <tfoot>\n                                        <tr><th colspan=\"5\" id=\"tfootMyProfileActiveSessions\"></th></tr>\n                                    </tfoot>\n                                </table>\n                            </div>\n\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\" onclick=\"saveMyProfile(this); return false;\">Save</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalCreateApiToken\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Create API Token</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divCreateApiTokenAlert\"></div>\n\n                        <div id=\"divCreateApiTokenLoader\" style=\"height: 350px;\"></div>\n\n                        <div id=\"divCreateApiTokenForm\">\n                            <div class=\"form-group\">\n                                <label for=\"txtCreateApiTokenUsername\" class=\"col-sm-4 control-label\">Username</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtCreateApiTokenUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\" disabled>\n                                    <select id=\"optCreateApiTokenUsername\" class=\"form-control\"></select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" id=\"divCreateApiTokenPassword\">\n                                <label for=\"txtCreateApiTokenPassword\" class=\"col-sm-4 control-label\">Password</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtCreateApiTokenPassword\" type=\"password\" class=\"form-control\" placeholder=\"password\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" id=\"divCreateApiToken2FAOTP\">\n                                <label for=\"txtCreateApiToken2FATOTP\" class=\"col-sm-4 control-label\">Enter OTP</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtCreateApiToken2FATOTP\" type=\"text\" class=\"form-control\" style=\"width: 100px;\" placeholder=\"OTP\" maxlength=\"6\">\n                                    <div style=\"padding-top: 5px;\">Enter the 6-digit code you see in your authenticator app.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtCreateApiTokenName\" class=\"col-sm-4 control-label\">Token Name</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtCreateApiTokenName\" type=\"text\" class=\"form-control\" placeholder=\"token name\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div>\n                                <b>Warning!</b> 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.\n                            </div>\n                        </div>\n\n                        <div id=\"divCreateApiTokenOutput\">\n                            <div class=\"form-group\">\n                                <label class=\"col-sm-3 control-label\">Username</label>\n                                <div class=\"col-sm-9\">\n                                    <div id=\"lblCreateApiTokenOutputUsername\" style=\"padding-top: 7px;\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label class=\"col-sm-3 control-label\">Token Name</label>\n                                <div class=\"col-sm-9\">\n                                    <div id=\"lblCreateApiTokenOutputTokenName\" style=\"padding-top: 7px; word-wrap: anywhere;\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label class=\"col-sm-3 control-label\">Token</label>\n                                <div class=\"col-sm-9\">\n                                    <div id=\"lblCreateApiTokenOutputToken\" style=\"padding-top: 7px;\"></div>\n                                </div>\n                            </div>\n\n                            <div>\n                                <b>Warning!</b> The token value above will not be displayed later. You must copy the token value immediately and save it for later use.\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnCreateApiToken\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Creating...\">Create</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalChangePassword\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 id=\"titleChangePassword\" class=\"modal-title\">Change Password</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divChangePasswordAlert\"></div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtChangePasswordUsername\" class=\"col-sm-4 control-label\">Username</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtChangePasswordUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\" disabled>\n                            </div>\n                        </div>\n\n                        <div id=\"divChangePasswordCurrentPassword\" class=\"form-group\">\n                            <label for=\"txtChangePasswordCurrentPassword\" class=\"col-sm-4 control-label\">Current Password</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtChangePasswordCurrentPassword\" type=\"password\" class=\"form-control\" placeholder=\"current password\" maxlength=\"255\">\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtChangePasswordNewPassword\" class=\"col-sm-4 control-label\">New Password</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtChangePasswordNewPassword\" type=\"password\" class=\"form-control\" placeholder=\"new password\" maxlength=\"255\">\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtChangePasswordConfirmPassword\" class=\"col-sm-4 control-label\">Confirm Password</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtChangePasswordConfirmPassword\" type=\"password\" class=\"form-control\" placeholder=\"confirm password\" maxlength=\"255\">\n                            </div>\n                        </div>\n\n                        <div id=\"divChangePassword2FATOTP\" class=\"form-group\">\n                            <label for=\"txtChangePassword2FATOTP\" class=\"col-sm-4 control-label\">Enter OTP</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtChangePassword2FATOTP\" type=\"text\" class=\"form-control\" style=\"width: 100px;\" placeholder=\"OTP\" maxlength=\"6\">\n                                <div style=\"padding-top: 5px;\">Enter the 6-digit code you see in your authenticator app.</div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnChangePassword\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Working...\">Change</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalConfigure2FA\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 750px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Configure Two-factor Authentication (2FA)</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divConfigure2FAAlert\"></div>\n\n                        <div id=\"divConfigure2FALoader\" style=\"height: 400px;\"></div>\n\n                        <div id=\"divConfigure2FAViewer\">\n                            <div>\n                                <p>When you enable Two-factor Authentication (2FA) using Time-based One-time Password (TOTP), you will be required to enter a one-time password (OTP) in addition to your password when you login. You will need to use an authenticator app like <b>Microsoft Authenticator</b> or <b>Google Authenticator</b> to generate the OTP when you login.</p>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtConfigure2FAUsername\" class=\"col-sm-3 control-label\">Username</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtConfigure2FAUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\" disabled>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"lblConfigure2FAStatus\" class=\"col-sm-3 control-label\">2FA Status</label>\n                                <div class=\"col-sm-8\">\n                                    <div id=\"lblConfigure2FAStatus\" style=\"padding: 6px 0; font-weight: bold;\"></div>\n                                </div>\n                            </div>\n\n                            <div id=\"divConfigure2FAInitialize\">\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-3 control-label\">Secret Key</label>\n                                    <div class=\"col-sm-8\">\n                                        <div id=\"lblConfigure2FAQRCode\" style=\"margin: -6px 0px 0px -12px;\"></div>\n                                        <div id=\"lblConfigure2FASecret\" style=\"padding: 4px 0; font-weight: bold;\"></div>\n                                        <div style=\"padding-top: 5px;\">Scan the QR Code or manually enter the secret key (spaces don't matter) given above in your authenticator app.</div>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtConfigure2FATOTP\" class=\"col-sm-3 control-label\">Enter OTP</label>\n                                    <div class=\"col-sm-8\">\n                                        <input id=\"txtConfigure2FATOTP\" type=\"text\" class=\"form-control\" style=\"width: 100px;\" placeholder=\"OTP\" maxlength=\"6\">\n                                        <div style=\"padding-top: 5px;\">Enter the 6-digit code you see in your authenticator app.</div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnEnable2FA\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Working...\" onclick=\"enable2FA(this); return false;\">Enable</button>\n                        <button id=\"btnDisable2FA\" type=\"button\" class=\"btn btn-warning\" data-loading-text=\"Working...\" onclick=\"disable2FA(this); return false;\">Disable</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalForgotPassword\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Forgot Password?</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <p>To reset your password, you need to contact the DNS server administrator.</p>\n                    <p>If you are an administrator, follow these steps to reset the 'admin' user's password:</p>\n                    <ol>\n                        <li>Stop the DNS server.</li>\n                        <li>Find the DNS Server config folder and locate the <b>auth.config</b> file. The config folder will be found where the DNS Server is installed on Windows or /etc/dns/ folder on Linux.</li>\n                        <li>Rename the <b>auth.config</b> file as <b>resetadmin.config</b></li>\n                        <li>Start the DNS Server.</li>\n                        <li>Just refresh this web page in the web browser to auto login with default credentials and quickly change the password.</li>\n                    </ol>\n                    <p>On Linux, stop the DNS server by running 'sudo systemctl stop dns' command and 'sudo systemctl start dns' command to start it.</p>\n                    <p>On Windows, press Win+R to open Run, enter 'services.msc', and press enter to open Services console. Find service named 'Technitium DNS Server' and use the Action menu to start/stop it.</p>\n                    <p><b>Note: </b>To reset 'admin' password, you will need file system access on the server running this DNS Server. If the 'admin' user does not exists then it will be created automatically. If the 'admin' user has Two-factor Authentication (2FA) configured then it will be disabled too.</p>\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalUpdateAvailable\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\" id=\"lblUpdateAvailableTitle\">New Update Available</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div style=\"margin-bottom: 20px;\">\n                        <div id=\"lblUpdateMessage\" style=\"margin-bottom: 10px;\"></div>\n                        <a id=\"lnkUpdateDownload\" href=\"#\" target=\"_blank\" style=\"font-weight: bold; font-size: 18px;\">Download Now!</a>\n                        <a id=\"lnkUpdateInstructions\" href=\"#\" target=\"_blank\" style=\"font-weight: bold; font-size: 18px;\">Update Instructions</a>\n                    </div>\n                    <div style=\"margin-bottom: 10px;\">\n                        <div style=\"font-weight: bold;\">Update Version: <span id=\"lblUpdateVersion\"></span></div>\n                        <div>Current Version: <span id=\"lblCurrentVersion\"></span></div>\n                    </div>\n                    <div style=\"margin-bottom: 10px;\">\n                        <a id=\"lnkUpdateChangeLog\" href=\"#\" target=\"_blank\" style=\"font-weight: bold;\">Read Change Logs</a>\n                    </div>\n                    <div style=\"margin-bottom: 10px;\">\n                        Note! It is highly recommended to Backup Settings before installing the update.\n                    </div>\n                    <div>\n                        Note! You will have to refresh this web page manually after updating the DNS server so that new UI changes are loaded.\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <div id=\"modalAddZone\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Add Zone</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divAddZoneAlert\"></div>\n\n                        <div style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <div class=\"form-group\">\n                                <label for=\"txtAddZone\" class=\"col-sm-4 control-label\">Zone</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtAddZone\" type=\"text\" class=\"form-control\" placeholder=\"example.com or 192.168.0.0/24 or 2001:db8::/64\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label class=\"col-sm-4 control-label\">Type</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" id=\"rdAddZoneTypePrimary\" value=\"Primary\" checked>\n                                            Primary Zone (default)\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" value=\"Secondary\">\n                                            Secondary Zone\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" value=\"Stub\">\n                                            Stub Zone\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" value=\"Forwarder\">\n                                            Conditional Forwarder Zone\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" value=\"SecondaryForwarder\">\n                                            Secondary Conditional Forwarder Zone\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" value=\"Catalog\">\n                                            Catalog Zone\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" value=\"SecondaryCatalog\">\n                                            Secondary Catalog Zone\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneType\" value=\"SecondaryRoot\">\n                                            Secondary ROOT Zone (<a href=\"https://datatracker.ietf.org/doc/rfc8806/\" target=\"_blank\">RFC 8806</a>)\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div class=\"form-group\" id=\"divAddZoneCatalogZone\">\n                                <label class=\"col-sm-4 control-label\">Catalog Zone</label>\n                                <div class=\"col-sm-7\">\n                                    <select id=\"optAddZoneCatalogZoneName\" class=\"form-control\"></select>\n                                    <div style=\"padding-top: 5px;\">Select a Catalog zone to register as its member zone.</div>\n                                </div>\n                            </div>\n\n\n                            <div class=\"form-group\" id=\"divAddZoneInitializeForwarder\">\n                                <label class=\"col-sm-4 control-label\">Conditional Forwarder</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"checkbox\" style=\"margin-bottom: 6px;\">\n                                        <label>\n                                            <input id=\"chkAddZoneInitializeForwarder\" type=\"checkbox\"> Initialize Forwarder (FWD) Record\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddZoneImportZoneFile\" class=\"form-group\">\n                                <label for=\"fileAddZoneImportZone\" class=\"col-sm-4 control-label\">Import Zone File (Optional)</label>\n                                <div class=\"col-sm-7\">\n                                    <input type=\"file\" class=\"form-control\" id=\"fileAddZoneImportZone\">\n                                </div>\n                            </div>\n\n\n                            <div class=\"form-group\" id=\"divAddZoneUseSoaSerialDateScheme\">\n                                <label class=\"col-sm-4 control-label\">Zone Serial</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"checkbox\">\n                                        <label>\n                                            <input id=\"chkAddZoneUseSoaSerialDateScheme\" type=\"checkbox\"> Use SOA Serial Date Scheme\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div class=\"form-group\" id=\"divAddZonePrimaryNameServerAddresses\">\n                                <label id=\"lblAddZonePrimaryNameServerAddresses\" for=\"txtAddZonePrimaryNameServerAddresses\" class=\"col-sm-4 control-label\">Primary Name Server Addresses (Optional)</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtAddZonePrimaryNameServerAddresses\" class=\"form-control\" rows=\"4\" spellcheck=\"false\" placeholder=\"192.168.1.1\n2001:db8::\nns1.example.com (192.168.1.1)\nns1.example.com ([2001:db8::])\n\"></textarea>\n                                    <div id=\"divAddZonePrimaryNameServerAddressesInfo\" style=\"padding-top: 5px;\">Enter the primary name server addresses to sync the zone from. When unspecified, the SOA Primary Name Server will be resolved and used.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" id=\"divAddZoneZoneTransferProtocol\">\n                                <label class=\"col-sm-4 control-label\">Zone Transfer Protocol</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneZoneTransferProtocol\" id=\"rdAddZoneZoneTransferProtocolTcp\" value=\"Tcp\" checked>\n                                            XFR-over-TCP (default)\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneZoneTransferProtocol\" value=\"Tls\">\n                                            XFR-over-TLS\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneZoneTransferProtocol\" value=\"Quic\">\n                                            XFR-over-QUIC\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div class=\"form-group\" id=\"divAddZoneTsigKeyName\">\n                                <label for=\"optAddZoneTsigKeyName\" class=\"col-sm-4 control-label\">TSIG Key Name (Optional)</label>\n                                <div class=\"col-sm-7\">\n                                    <select id=\"optAddZoneTsigKeyName\" class=\"form-control\"></select>\n                                </div>\n                            </div>\n\n\n                            <div class=\"form-group\" id=\"divAddZoneValidateZone\">\n                                <label class=\"col-sm-4 control-label\">Zone Validation</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"checkbox\">\n                                        <label>\n                                            <input id=\"chkAddZoneValidateZone\" type=\"checkbox\"> Use <a href=\"https://datatracker.ietf.org/doc/rfc8976/\" target=\"_blank\">ZONEMD</a> to Validate Zone\n                                        </label>\n                                        <div style=\"padding-top: 5px; padding-left: 20px;\">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.</div>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div class=\"form-group\" id=\"divAddZoneForwarderProtocol\">\n                                <label class=\"col-sm-4 control-label\">Protocol</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneForwarderProtocol\" id=\"rdAddZoneForwarderProtocolUdp\" value=\"Udp\" checked>\n                                            DNS-over-UDP (default)\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneForwarderProtocol\" value=\"Tcp\">\n                                            DNS-over-TCP\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneForwarderProtocol\" value=\"Tls\">\n                                            DNS-over-TLS\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneForwarderProtocol\" value=\"Https\">\n                                            DNS-over-HTTPS\n                                        </label>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdAddZoneForwarderProtocol\" value=\"Quic\">\n                                            DNS-over-QUIC\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" id=\"divAddZoneForwarder\">\n                                <label for=\"txtAddZoneForwarder\" class=\"col-sm-4 control-label\">Forwarder</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"checkbox\">\n                                        <label>\n                                            <input id=\"chkAddZoneForwarderThisServer\" type=\"checkbox\" onclick=\"updateAddZoneFormForwarderThisServer();\"> Use \"This Server\"\n                                        </label>\n                                    </div>\n                                    <div style=\"padding-top: 5px; padding-left: 20px; padding-bottom: 10px;\">\n                                        When using \"This Server\", if a record does not exists in the zone then the request is forwarded to the DNS server's resolver internally. This allows you to override any record for the forwarded domain name or control its DNSSEC validation.\n                                    </div>\n\n                                    <input id=\"txtAddZoneForwarder\" type=\"text\" class=\"form-control\" placeholder=\"8.8.8.8\">\n\n                                    <div style=\"padding-top: 5px;\">Enter a forwarder server address above. You can add more forwarders by adding FWD records after the zone is added.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" id=\"divAddZoneForwarderDnssecValidation\">\n                                <label class=\"col-sm-4 control-label\">DNSSEC</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"checkbox\" style=\"margin-bottom: 6px;\">\n                                        <label>\n                                            <input id=\"chkAddZoneForwarderDnssecValidation\" type=\"checkbox\"> Enable DNSSEC Validation\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div id=\"divAddZoneForwarderProxy\">\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-4 control-label\">Network Proxy</label>\n                                    <div class=\"col-sm-7\">\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddZoneForwarderProxyType\" value=\"NoProxy\">\n                                                No Proxy\n                                            </label>\n                                        </div>\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddZoneForwarderProxyType\" id=\"rdAddZoneForwarderProxyTypeDefaultProxy\" value=\"DefaultProxy\" checked>\n                                                Default Proxy (default)\n                                            </label>\n                                        </div>\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddZoneForwarderProxyType\" value=\"Http\">\n                                                HTTP Proxy\n                                            </label>\n                                        </div>\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddZoneForwarderProxyType\" value=\"Socks5\">\n                                                SOCKS5 Proxy\n                                            </label>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddZoneForwarderProxyAddress\" class=\"col-sm-4 control-label\">Proxy Server Address</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddZoneForwarderProxyAddress\" type=\"text\" class=\"form-control\" placeholder=\"domain name or IP address\">\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddZoneForwarderProxyPort\" class=\"col-sm-4 control-label\">Proxy Server Port</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddZoneForwarderProxyPort\" type=\"number\" class=\"form-control\" placeholder=\"port\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddZoneForwarderProxyUsername\" class=\"col-sm-4 control-label\">Proxy Server Username</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddZoneForwarderProxyUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\">\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddZoneForwarderProxyPassword\" class=\"col-sm-4 control-label\">Proxy Server Password</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddZoneForwarderProxyPassword\" type=\"password\" class=\"form-control\" placeholder=\"password\">\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <div class=\"pull-left\" style=\"text-align: left;\">\n                            <a href=\"https://blog.technitium.com/2022/06/how-to-self-host-your-own-domain-name.html\" target=\"_blank\">Help: How To Self Host Your Own Domain Name</a>\n                        </div>\n                        <div class=\"pull-right\">\n                            <button id=\"btnAddZone\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Adding...\" onclick=\"addZone(); return false;\">Add</button>\n                            <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalAddEditRecord\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 id=\"titleAddEditRecord\" class=\"modal-title\">Add Edit Record</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divAddEditRecordAlert\"></div>\n\n                        <div style=\"max-height: 500px; overflow-y: auto; padding: 0 15px; overflow-x: hidden;\">\n\n                            <div class=\"form-group\">\n                                <label for=\"txtAddEditRecordName\" class=\"col-sm-4 control-label\">Name</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtAddEditRecordName\" type=\"text\" class=\"form-control\" placeholder=\"@\">\n                                    <div style=\"margin-top: 6px; font-weight: 700;\">.<span id=\"lblAddEditRecordZoneName\">example.com</span></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"optAddEditRecordType\" class=\"col-sm-4 control-label\">Type</label>\n                                <div class=\"col-sm-7\">\n                                    <select id=\"optAddEditRecordType\" class=\"form-control\" onchange=\"modifyAddRecordFormByType(true);\" style=\"width: auto;\">\n                                        <option>A</option>\n                                        <option>NS</option>\n                                        <option id=\"optEditRecordTypeSoa\">SOA</option>\n                                        <option>CNAME</option>\n                                        <option>PTR</option>\n                                        <option>MX</option>\n                                        <option>TXT</option>\n                                        <option>RP</option>\n                                        <option>AAAA</option>\n                                        <option>SRV</option>\n                                        <option>NAPTR</option>\n                                        <option>DNAME</option>\n                                        <option id=\"optAddEditRecordTypeDs\">DS</option>\n                                        <option id=\"optAddEditRecordTypeSshfp\">SSHFP</option>\n                                        <option id=\"optAddEditRecordTypeTlsa\">TLSA</option>\n                                        <option>SVCB</option>\n                                        <option>HTTPS</option>\n                                        <option>URI</option>\n                                        <option>CAA</option>\n                                        <option id=\"optAddEditRecordTypeAName\">ANAME</option>\n                                        <option id=\"optAddEditRecordTypeFwd\">FWD</option>\n                                        <option id=\"optAddEditRecordTypeApp\">APP</option>\n                                        <option>Unknown</option>\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtAddEditRecordTtl\" class=\"col-sm-4 control-label\">TTL</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtAddEditRecordTtl\" type=\"text\" class=\"form-control\" placeholder=\"3600\" style=\"width: 100px; display: inline;\">\n                                    <span id=\"spanAddEditRecordTtlUnit\">seconds (default 3600/1h)</span>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordData\">\n                                <div id=\"divAddEditRecordDataUnknownType\" class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataUnknownType\" class=\"col-sm-4 control-label\">RR Type</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataUnknownType\" type=\"text\" class=\"form-control\" placeholder=\"type\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label id=\"lblAddEditRecordDataValue\" for=\"txtAddEditRecordDataValue\" class=\"col-sm-4 control-label\">Value</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataValue\" type=\"text\" class=\"form-control\">\n                                    </div>\n                                </div>\n\n                                <div id=\"divAddEditRecordDataPtr\" class=\"form-group\">\n                                    <div class=\"col-sm-offset-4 col-sm-7\">\n                                        <div class=\"checkbox\">\n                                            <label>\n                                                <input id=\"chkAddEditRecordDataPtr\" type=\"checkbox\"> <span id=\"chkAddEditRecordDataPtrLabel\">Add reverse (PTR) record</span>\n                                            </label>\n                                        </div>\n\n                                        <div class=\"checkbox\">\n                                            <label>\n                                                <input id=\"chkAddEditRecordDataCreatePtrZone\" type=\"checkbox\"> Create reverse zone for PTR record\n                                            </label>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataNs\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNsNameServer\" class=\"col-sm-4 control-label\">Name Server</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataNsNameServer\" type=\"text\" class=\"form-control\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNsGlue\" class=\"col-sm-4 control-label\">Glue Addresses</label>\n                                    <div class=\"col-sm-7\">\n                                        <textarea id=\"txtAddEditRecordDataNsGlue\" class=\"form-control\" rows=\"3\" spellcheck=\"false\" placeholder=\"192.168.1.1\n2001:db8::\"></textarea>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divEditRecordDataSoa\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditRecordDataSoaPrimaryNameServer\" class=\"col-sm-4 control-label\">Primary Name Server</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditRecordDataSoaPrimaryNameServer\" type=\"text\" class=\"form-control\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditRecordDataSoaResponsiblePerson\" class=\"col-sm-4 control-label\">Responsible Person</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditRecordDataSoaResponsiblePerson\" type=\"text\" class=\"form-control\" placeholder=\"email address\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditRecordDataSoaSerial\" class=\"col-sm-4 control-label\">Serial</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditRecordDataSoaSerial\" type=\"number\" class=\"form-control\" style=\"width: 150px;\">\n                                    </div>\n                                    <div id=\"divEditRecordDataSoaUseSerialDateScheme\" class=\"col-sm-offset-4 col-sm-7\">\n                                        <div class=\"checkbox\">\n                                            <label>\n                                                <input id=\"chkEditRecordDataSoaUseSerialDateScheme\" type=\"checkbox\"> Use Serial Date Scheme\n                                            </label>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditRecordDataSoaRefresh\" class=\"col-sm-4 control-label\">Refresh</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditRecordDataSoaRefresh\" type=\"text\" class=\"form-control\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds</span>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditRecordDataSoaRetry\" class=\"col-sm-4 control-label\">Retry</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditRecordDataSoaRetry\" type=\"text\" class=\"form-control\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds</span>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditRecordDataSoaExpire\" class=\"col-sm-4 control-label\">Expire</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditRecordDataSoaExpire\" type=\"text\" class=\"form-control\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds</span>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditRecordDataSoaMinimum\" class=\"col-sm-4 control-label\">Minimum</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditRecordDataSoaMinimum\" type=\"text\" class=\"form-control\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds</span>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataMx\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataMxPreference\" class=\"col-sm-4 control-label\">Preference</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataMxPreference\" type=\"number\" class=\"form-control\" placeholder=\"1\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataMxExchange\" class=\"col-sm-4 control-label\">Exchange</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataMxExchange\" type=\"text\" class=\"form-control\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataTxt\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataTxt\" class=\"col-sm-4 control-label\">Text Data</label>\n                                    <div class=\"col-sm-7\">\n                                        <textarea id=\"txtAddEditRecordDataTxt\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                        <div class=\"checkbox\">\n                                            <label>\n                                                <input id=\"chkAddEditRecordDataTxtSplitText\" type=\"checkbox\"> Use New Line To Split Text Into Multiple Character-Strings\n                                            </label>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataRp\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataRpMailbox\" class=\"col-sm-4 control-label\">Mailbox</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataRpMailbox\" type=\"text\" class=\"form-control\" placeholder=\"email address\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataRpTxtDomain\" class=\"col-sm-4 control-label\">TXT Domain</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataRpTxtDomain\" type=\"text\" class=\"form-control\" placeholder=\".\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataSrv\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataSrvPriority\" class=\"col-sm-4 control-label\">Priority</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataSrvPriority\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataSrvWeight\" class=\"col-sm-4 control-label\">Weight</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataSrvWeight\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataSrvPort\" class=\"col-sm-4 control-label\">Port</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataSrvPort\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataSrvTarget\" class=\"col-sm-4 control-label\">Target</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataSrvTarget\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataNaptr\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNaptrOrder\" class=\"col-sm-4 control-label\">Order</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataNaptrOrder\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNaptrPreference\" class=\"col-sm-4 control-label\">Preference</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataNaptrPreference\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNaptrFlags\" class=\"col-sm-4 control-label\">Flags</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataNaptrFlags\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNaptrServices\" class=\"col-sm-4 control-label\">Services</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataNaptrServices\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNaptrRegExp\" class=\"col-sm-4 control-label\">Regular Expression</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataNaptrRegExp\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataNaptrReplacement\" class=\"col-sm-4 control-label\">Replacement</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataNaptrReplacement\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataDs\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataDsKeyTag\" class=\"col-sm-4 control-label\">Key Tag</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataDsKeyTag\" placeholder=\"key tag\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataDsAlgorithm\" class=\"col-sm-4 control-label\">DNSSEC Algorithm</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataDsAlgorithm\" class=\"form-control\">\n                                            <option value=\"RSAMD5\">RSAMD5 (1)</option>\n                                            <option value=\"RSASHA1\">RSASHA1 (5)</option>\n                                            <option value=\"RSASHA256\">RSASHA256 (8)</option>\n                                            <option value=\"RSASHA512\">RSASHA512 (10)</option>\n                                            <option value=\"ECDSAP256SHA256\">ECDSAP256SHA256 (13)</option>\n                                            <option value=\"ECDSAP384SHA384\">ECDSAP384SHA384 (14)</option>\n                                            <option value=\"ED25519\">ED25519 (15)</option>\n                                            <option value=\"ED448\">ED448 (16)</option>\n                                        </select>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataDsDigestType\" class=\"col-sm-4 control-label\">Digest Type</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataDsDigestType\" class=\"form-control\">\n                                            <option value=\"SHA1\">SHA1 (1)</option>\n                                            <option value=\"SHA256\">SHA256 (2)</option>\n                                            <option value=\"SHA384\">SHA384 (4)</option>\n                                        </select>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataDsDigest\" class=\"col-sm-4 control-label\">Digest</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataDsDigest\" placeholder=\"hash string\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataSshfp\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataSshfpAlgorithm\" class=\"col-sm-4 control-label\">Algorithm</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataSshfpAlgorithm\" class=\"form-control\">\n                                            <option>RSA</option>\n                                            <option>DSA</option>\n                                            <option>ECDSA</option>\n                                            <option>Ed25519</option>\n                                            <option>Ed448</option>\n                                        </select>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataSshfpFingerprintType\" class=\"col-sm-4 control-label\">Fingerprint Type</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataSshfpFingerprintType\" class=\"form-control\">\n                                            <option>SHA1</option>\n                                            <option>SHA256</option>\n                                        </select>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataSshfpFingerprint\" class=\"col-sm-4 control-label\">Fingerprint</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataSshfpFingerprint\" placeholder=\"hash string\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataTlsa\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataTlsaCertificateUsage\" class=\"col-sm-4 control-label\">Certificate Usage</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataTlsaCertificateUsage\" class=\"form-control\">\n                                            <option>PKIX-TA</option>\n                                            <option>PKIX-EE</option>\n                                            <option>DANE-TA</option>\n                                            <option>DANE-EE</option>\n                                        </select>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataTlsaSelector\" class=\"col-sm-4 control-label\">Selector</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataTlsaSelector\" class=\"form-control\">\n                                            <option>Cert</option>\n                                            <option>SPKI</option>\n                                        </select>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataTlsaMatchingType\" class=\"col-sm-4 control-label\">Matching Type</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataTlsaMatchingType\" class=\"form-control\">\n                                            <option>Full</option>\n                                            <option>SHA2-256</option>\n                                            <option>SHA2-512</option>\n                                        </select>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataTlsaCertificateAssociationData\" class=\"col-sm-4 control-label\">Certificate Association Data</label>\n                                    <div class=\"col-sm-7\">\n                                        <textarea id=\"txtAddEditRecordDataTlsaCertificateAssociationData\" class=\"form-control\" rows=\"6\" spellcheck=\"false\" placeholder=\"5F95253A20A0957648DEBAAEB032F7C5720CD4F0DCF928840C55650687921DAE\n          OR\n-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----\n\"></textarea>\n                                        <div style=\"padding-top: 5px;\">Enter either a hash value that you have independently generated, OR enter the certificate in PEM format to automatically generate the association data based on the Selector and Matching Type values.</div>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataSvcb\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataSvcbPriority\" class=\"col-sm-4 control-label\">Priority</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataSvcbPriority\" style=\"width: 100px; display: inline;\">\n                                        <span>(set 0 for alias mode)</span>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataSvcbTargetName\" class=\"col-sm-4 control-label\">Target Name</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataSvcbTargetName\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-4 control-label\">Params</label>\n                                    <div class=\"col-sm-7\">\n                                        <table class=\"table\" style=\"margin-bottom: 0px;\">\n                                            <thead>\n                                                <tr>\n                                                    <th>Key</th>\n                                                    <th>Value</th>\n                                                    <th style=\"width: 94px;\">\n                                                        <button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 25px;\" onclick=\"addSvcbRecordParamEditRow('', '');\">Add</button>\n                                                    </th>\n                                                </tr>\n                                            </thead>\n                                            <tbody id=\"tableAddEditRecordDataSvcbParams\">\n                                            </tbody>\n                                        </table>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-4 control-label\">Automatic Hints</label>\n                                    <div class=\"col-sm-7\">\n                                        <div class=\"checkbox\">\n                                            <label>\n                                                <input id=\"chkAddEditRecordDataSvcbAutoIpv4Hint\" type=\"checkbox\"> Use Automatic IPv4 Hint\n                                            </label>\n                                        </div>\n                                    </div>\n                                    <div class=\"col-sm-offset-4 col-sm-7\">\n                                        <div class=\"checkbox\">\n                                            <label>\n                                                <input id=\"chkAddEditRecordDataSvcbAutoIpv6Hint\" type=\"checkbox\"> Use Automatic IPv6 Hint\n                                            </label>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataUri\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataUriPriority\" class=\"col-sm-4 control-label\">Priority</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataUriPriority\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataUriWeight\" class=\"col-sm-4 control-label\">Weight</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataUriWeight\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataUri\" class=\"col-sm-4 control-label\">URI</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataUri\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataCaa\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataCaaFlags\" class=\"col-sm-4 control-label\">Flags</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"number\" class=\"form-control\" id=\"txtAddEditRecordDataCaaFlags\" placeholder=\"0\" style=\"width: 100px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataCaaTag\" class=\"col-sm-4 control-label\">Tag</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataCaaTag\" placeholder=\"issue\" style=\"width: 150px;\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataCaaValue\" class=\"col-sm-4 control-label\">Authority</label>\n                                    <div class=\"col-sm-7\">\n                                        <input type=\"text\" class=\"form-control\" id=\"txtAddEditRecordDataCaaValue\">\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataForwarder\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-4 control-label\">Protocol</label>\n                                    <div class=\"col-sm-7\">\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProtocol\" id=\"rdAddEditRecordDataForwarderProtocolUdp\" value=\"Udp\" checked>\n                                                DNS-over-UDP (default)\n                                            </label>\n                                        </div>\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProtocol\" id=\"rdAddEditRecordDataForwarderProtocolTcp\" value=\"Tcp\">\n                                                DNS-over-TCP\n                                            </label>\n                                        </div>\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProtocol\" id=\"rdAddEditRecordDataForwarderProtocolTls\" value=\"Tls\">\n                                                DNS-over-TLS\n                                            </label>\n                                        </div>\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProtocol\" id=\"rdAddEditRecordDataForwarderProtocolHttps\" value=\"Https\">\n                                                DNS-over-HTTPS\n                                            </label>\n                                        </div>\n                                        <div class=\"radio\">\n                                            <label>\n                                                <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProtocol\" id=\"rdAddEditRecordDataForwarderProtocolQuic\" value=\"Quic\">\n                                                DNS-over-QUIC\n                                            </label>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataForwarder\" class=\"col-sm-4 control-label\">Forwarder</label>\n                                    <div class=\"col-sm-7\">\n                                        <div class=\"checkbox\">\n                                            <label>\n                                                <input id=\"chkAddEditRecordDataForwarderThisServer\" type=\"checkbox\" onclick=\"updateAddEditFormForwarderThisServer();\"> Use \"This Server\"\n                                            </label>\n                                        </div>\n                                        <div style=\"padding-top: 5px; padding-left: 20px; padding-bottom: 10px;\">\n                                            When using \"This Server\", if a record does not exists in the zone then the request is forwarded to the DNS server's resolver internally. This allows you to override any record for the forwarded domain name or control its DNSSEC validation.\n                                        </div>\n\n                                        <input id=\"txtAddEditRecordDataForwarder\" type=\"text\" class=\"form-control\" placeholder=\"8.8.8.8\">\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-4 control-label\">Priority</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtAddEditRecordDataForwarderPriority\" type=\"number\" class=\"form-control\" placeholder=\"0\" style=\"width: 100px; display: inline;\">\n                                        <span>(valid range 0-255; default 0)</span>\n                                    </div>\n                                    <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                        Forwarders are sorted by priority value i.e. forwarder with low priority value will be queried before trying for forwarder with high priority value. Forwarders with the same priority value will be queried concurrently.\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-4 control-label\">DNSSEC</label>\n                                    <div class=\"col-sm-7\">\n                                        <div class=\"checkbox\" style=\"margin-bottom: 6px;\">\n                                            <label>\n                                                <input id=\"chkAddEditRecordDataForwarderDnssecValidation\" type=\"checkbox\"> Enable DNSSEC Validation\n                                            </label>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div id=\"divAddEditRecordDataForwarderProxy\">\n                                    <div class=\"form-group\">\n                                        <label class=\"col-sm-4 control-label\">Network Proxy</label>\n                                        <div class=\"col-sm-7\">\n                                            <div class=\"radio\">\n                                                <label>\n                                                    <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProxyType\" id=\"rdAddEditRecordDataForwarderProxyTypeNoProxy\" value=\"NoProxy\">\n                                                    No Proxy\n                                                </label>\n                                            </div>\n                                            <div class=\"radio\">\n                                                <label>\n                                                    <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProxyType\" id=\"rdAddEditRecordDataForwarderProxyTypeDefaultProxy\" value=\"DefaultProxy\" checked>\n                                                    Default Proxy (default)\n                                                </label>\n                                            </div>\n                                            <div class=\"radio\">\n                                                <label>\n                                                    <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProxyType\" id=\"rdAddEditRecordDataForwarderProxyTypeHttp\" value=\"Http\">\n                                                    HTTP Proxy\n                                                </label>\n                                            </div>\n                                            <div class=\"radio\">\n                                                <label>\n                                                    <input type=\"radio\" name=\"rdAddEditRecordDataForwarderProxyType\" id=\"rdAddEditRecordDataForwarderProxyTypeSocks5\" value=\"Socks5\">\n                                                    SOCKS5 Proxy\n                                                </label>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group\">\n                                        <label for=\"txtAddEditRecordDataForwarderProxyAddress\" class=\"col-sm-4 control-label\">Proxy Server Address</label>\n                                        <div class=\"col-sm-7\">\n                                            <input id=\"txtAddEditRecordDataForwarderProxyAddress\" type=\"text\" class=\"form-control\" placeholder=\"domain name or IP address\">\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group\">\n                                        <label for=\"txtAddEditRecordDataForwarderProxyPort\" class=\"col-sm-4 control-label\">Proxy Server Port</label>\n                                        <div class=\"col-sm-7\">\n                                            <input id=\"txtAddEditRecordDataForwarderProxyPort\" type=\"number\" class=\"form-control\" placeholder=\"port\" style=\"width: 100px;\">\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group\">\n                                        <label for=\"txtAddEditRecordDataForwarderProxyUsername\" class=\"col-sm-4 control-label\">Proxy Server Username</label>\n                                        <div class=\"col-sm-7\">\n                                            <input id=\"txtAddEditRecordDataForwarderProxyUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\">\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group\">\n                                        <label for=\"txtAddEditRecordDataForwarderProxyPassword\" class=\"col-sm-4 control-label\">Proxy Server Password</label>\n                                        <div class=\"col-sm-7\">\n                                            <input id=\"txtAddEditRecordDataForwarderProxyPassword\" type=\"password\" class=\"form-control\" placeholder=\"password\">\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordDataApplication\" style=\"display: none;\">\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataAppName\" class=\"col-sm-4 control-label\">App Name</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataAppName\" class=\"form-control\"></select>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"optAddEditRecordDataClassPath\" class=\"col-sm-4 control-label\">Class Path</label>\n                                    <div class=\"col-sm-7\">\n                                        <select id=\"optAddEditRecordDataClassPath\" class=\"form-control\"></select>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtAddEditRecordDataData\" class=\"col-sm-4 control-label\">Record Data (if any)</label>\n                                    <div class=\"col-sm-7\">\n                                        <textarea id=\"txtAddEditRecordDataData\" class=\"form-control\" rows=\"6\" spellcheck=\"false\"></textarea>\n                                    </div>\n                                </div>\n                            </div>\n\n\n                            <div id=\"divAddEditRecordOverwrite\" class=\"form-group\">\n                                <div class=\"col-sm-offset-4 col-sm-7\">\n                                    <div class=\"checkbox\">\n                                        <label>\n                                            <input id=\"chkAddEditRecordOverwrite\" type=\"checkbox\"> Overwrite existing records\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtAddEditRecordComments\" class=\"col-sm-4 control-label\">Comments</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtAddEditRecordComments\" class=\"form-control\" rows=\"3\" maxlength=\"255\"></textarea>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" id=\"divAddEditRecordExpiryTtl\">\n                                <label for=\"txtAddEditRecordExpiryTtl\" class=\"col-sm-4 control-label\">Expiry TTL</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtAddEditRecordExpiryTtl\" type=\"text\" class=\"form-control\" placeholder=\"0\" style=\"width: 100px; display: inline;\">\n                                    <span>seconds (set 0 to disable)</span>\n                                </div>\n                                <div class=\"col-sm-offset-4 col-sm-7\" style=\"padding-top: 5px;\">Set to automatically delete the record when the value in seconds elapses since the record’s last modified time.</div>\n                            </div>\n\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button type=\"submit\" class=\"btn btn-primary\" id=\"btnAddEditRecord\" data-loading-text=\"Saving...\">Save</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalImportZone\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Import - <span id=\"lblImportZoneName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divImportZoneAlert\"></div>\n\n                    <div class=\"form-horizontal\">\n                        <div class=\"form-group\">\n                            <label class=\"col-sm-4 control-label\">Import Options</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkImportZoneOverwrite\" type=\"checkbox\"> Overwrite existing records\n                                    </label>\n                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to overwrite existing records for the record types being imported.</div>\n                                </div>\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkImportZoneOverwriteSoaSerial\" type=\"checkbox\"> Overwrite SOA Serial\n                                    </label>\n                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enable this option to overwrite existing SOA record serial with the imported SOA record serial.</div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label class=\"col-sm-4 control-label\">Import Type</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdImportZoneType\" id=\"rdImportZoneTypeFile\" value=\"File\">\n                                        Zone File\n                                    </label>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdImportZoneType\" id=\"rdImportZoneTypeText\" value=\"Text\">\n                                        Text Editor\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divImportZoneFile\" class=\"form-group\">\n                            <label for=\"fileImportZone\" class=\"col-sm-4 control-label\">Zone File</label>\n                            <div class=\"col-sm-6\">\n                                <input type=\"file\" class=\"form-control\" id=\"fileImportZone\">\n                            </div>\n                        </div>\n                    </div>\n\n                    <div id=\"divImportZoneTextEditor\">\n                        <div class=\"form-group\">\n                            <label for=\"txtImportZoneText\" class=\"control-label\">Text Editor</label>\n                            <textarea id=\"txtImportZoneText\" class=\"form-control\" rows=\"15\" spellcheck=\"false\"></textarea>\n                            <div style=\"padding-top: 5px;\">Enter the records to be imported above in standard zone file format.</div>\n                        </div>\n                    </div>\n\n                    <p>Note! The <code>$ORIGIN</code> and <code>$TTL</code> values will be automatically set if not specified.</p>\n                    <p>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.</p>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnImportZone\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Importing...\" onclick=\"importZone(); return false;\">Import</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalCloneZone\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form>\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Clone Zone - <span id=\"lblCloneZoneZoneName\"></span></h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divCloneZoneAlert\"></div>\n                        <div class=\"form-horizontal\">\n                            <div class=\"form-group\">\n                                <label for=\"txtCloneZoneSourceZoneName\" class=\"col-sm-4 control-label\">Source Zone</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtCloneZoneSourceZoneName\" type=\"text\" class=\"form-control\" disabled>\n                                </div>\n                            </div>\n                            <div class=\"form-group\">\n                                <label for=\"txtCloneZoneZoneName\" class=\"col-sm-4 control-label\">New Zone</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtCloneZoneZoneName\" type=\"text\" class=\"form-control\" placeholder=\"example.com\">\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnCloneZone\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Cloning...\" onclick=\"cloneZone(this); return false;\">Clone Zone</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalConvertZone\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Convert Zone - <span id=\"lblConvertZoneZoneName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divConvertZoneAlert\"></div>\n                    <div class=\"form-horizontal\">\n                        <div class=\"form-group\">\n                            <label class=\"col-sm-4 control-label\">Convert To</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdConvertZoneToType\" id=\"rdConvertZoneToTypePrimary\" value=\"Primary\" />\n                                        Primary Zone\n                                    </label>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdConvertZoneToType\" id=\"rdConvertZoneToTypeForwarder\" value=\"Forwarder\" />\n                                        Conditional Forwarder Zone\n                                    </label>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdConvertZoneToType\" id=\"rdConvertZoneToTypeCatalog\" value=\"Catalog\" />\n                                        Catalog Zone\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n\n                        <p>\n                            <b>Note!</b> The conversion process may take a while depending on the number of records the zone has. When converting a Secondary Catalog zone to a Catalog zone, all member zones too will be converted to either Primary or Conditional Forwarder zone depending on their existing zone type. Please be patient till the conversion process completes.\n                        </p>\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnConvertZone\" type=\"submit\" class=\"btn btn-warning\" data-loading-text=\"Converting...\" onclick=\"convertZone(this); return false;\">Convert Zone</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalZoneOptions\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Zone Options - <span id=\"lblZoneOptionsZoneName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divZoneOptionsAlert\"></div>\n\n                    <div id=\"divZoneOptionsLoader\" style=\"height: 500px;\"></div>\n\n                    <div id=\"divZoneOptions\" style=\"max-height: 500px;\">\n\n                        <div>\n                            <ul class=\"nav nav-tabs\" role=\"tablist\">\n                                <li id=\"tabListZoneOptionsGeneral\" role=\"presentation\" class=\"active\"><a href=\"#tabPaneZoneOptionsGeneral\" aria-controls=\"tabPaneZoneOptionsGeneral\" role=\"tab\" data-toggle=\"tab\">General</a></li>\n                                <li id=\"tabListZoneOptionsQueryAccess\" role=\"presentation\"><a href=\"#tabPaneZoneOptionsQueryAccess\" aria-controls=\"tabPaneZoneOptionsQueryAccess\" role=\"tab\" data-toggle=\"tab\">Query Access</a></li>\n                                <li id=\"tabListZoneOptionsZoneTranfer\" role=\"presentation\"><a href=\"#tabPaneZoneOptionsZoneTransfer\" aria-controls=\"tabPaneZoneOptionsZoneTransfer\" role=\"tab\" data-toggle=\"tab\">Zone Transfer</a></li>\n                                <li id=\"tabListZoneOptionsNotify\" role=\"presentation\"><a href=\"#tabPaneZoneOptionsNotify\" aria-controls=\"tabPaneZoneOptionsNotify\" role=\"tab\" data-toggle=\"tab\">Notify</a></li>\n                                <li id=\"tabListZoneOptionsUpdate\" role=\"presentation\"><a href=\"#tabPaneZoneOptionsUpdate\" aria-controls=\"tabPaneZoneOptionsUpdate\" role=\"tab\" data-toggle=\"tab\">Dynamic Updates (RFC 2136)</a></li>\n                            </ul>\n\n                            <div class=\"tab-content\">\n                                <div id=\"tabPaneZoneOptionsGeneral\" role=\"tabpanel\" class=\"tab-pane active\" style=\"padding: 0 6px 0 0; max-height: 450px; margin: 10px 0 0 0; overflow-y: auto; overflow-x: hidden;\">\n                                    <div class=\"well well-sm form-horizontal\" id=\"divZoneOptionsGeneralCatalogZone\">\n                                        <div class=\"form-group\">\n                                            <label for=\"optZoneOptionsCatalogZoneName\" class=\"col-sm-4 control-label\">Catalog Zone</label>\n                                            <div class=\"col-sm-7\">\n                                                <select id=\"optZoneOptionsCatalogZoneName\" class=\"form-control\"></select>\n                                                <div style=\"padding-top: 5px;\">Select a Catalog zone to register as its member zone.</div>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"divZoneOptionsCatalogOverrideOptions\" class=\"form-group\">\n                                            <label class=\"col-sm-4 control-label\">Override Options</label>\n                                            <div class=\"col-sm-7\">\n                                                <div class=\"checkbox\">\n                                                    <label>\n                                                        <input id=\"chkZoneOptionsCatalogOverrideQueryAccess\" type=\"checkbox\"> Override Query Access Option\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enable to override Query Access option in the Catalog zone.</div>\n                                                </div>\n                                                <div class=\"checkbox\" id=\"divZoneOptionsCatalogOverrideZoneTransfer\">\n                                                    <label>\n                                                        <input id=\"chkZoneOptionsCatalogOverrideZoneTransfer\" type=\"checkbox\"> Override Zone Transfer Option\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enable to override Zone Transfer option in the Catalog zone.</div>\n                                                </div>\n                                                <div class=\"checkbox\" id=\"divZoneOptionsCatalogOverrideNotify\">\n                                                    <label>\n                                                        <input id=\"chkZoneOptionsCatalogOverrideNotify\" type=\"checkbox\"> Override Notify Option\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Enable to override Notify option in the Catalog zone.</div>\n                                                </div>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"divZoneOptionsCatalogNotifyFailedNameServers\" class=\"form-group\" style=\"display: none;\">\n                                            <label class=\"col-sm-4 control-label\">Notify Failed Name Servers</label>\n                                            <div class=\"col-sm-7\">\n                                                <span id=\"lblZoneOptionsCatalogNotifyFailedNameServers\" class=\"form-control\" style=\"height: auto;\"></span>\n                                            </div>\n                                        </div>\n\n                                        <div>Note! When a zone becomes a member of a Catalog zone, all of the Catalog zone's Options are inherited unless they are explicitly overridden using the Override Options.</div>\n                                    </div>\n\n                                    <div class=\"well well-sm form-horizontal\" id=\"divZoneOptionsGeneralPrimaryServer\">\n                                        <div class=\"form-group\">\n                                            <label id=\"lblZoneOptionsPrimaryNameServerAddresses\" for=\"txtZoneOptionsPrimaryNameServerAddresses\" class=\"col-sm-4 control-label\">Primary Name Server Addresses (Optional)</label>\n                                            <div class=\"col-sm-7\">\n                                                <textarea id=\"txtZoneOptionsPrimaryNameServerAddresses\" class=\"form-control\" rows=\"4\" spellcheck=\"false\" placeholder=\"192.168.1.1\n2001:db8::\nns1.example.com (192.168.1.1)\nns1.example.com ([2001:db8::])\n\"></textarea>\n                                                <div id=\"divZoneOptionsPrimaryNameServerAddressesInfo\" style=\"padding-top: 5px;\">Enter the primary name server addresses to sync the zone from. When unspecified, the SOA Primary Name Server will be resolved and used.</div>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group\" id=\"divZoneOptionsPrimaryServerZoneTransferProtocol\">\n                                            <label class=\"col-sm-4 control-label\">Zone Transfer Protocol</label>\n                                            <div class=\"col-sm-7\">\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdPrimaryZoneTransferProtocol\" id=\"rdPrimaryZoneTransferProtocolTcp\" value=\"Tcp\" checked>\n                                                        XFR-over-TCP (default)\n                                                    </label>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdPrimaryZoneTransferProtocol\" id=\"rdPrimaryZoneTransferProtocolTls\" value=\"Tls\">\n                                                        XFR-over-TLS\n                                                    </label>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdPrimaryZoneTransferProtocol\" id=\"rdPrimaryZoneTransferProtocolQuic\" value=\"Quic\">\n                                                        XFR-over-QUIC\n                                                    </label>\n                                                </div>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group\" id=\"divZoneOptionsPrimaryServerZoneTransferTsigKeyName\">\n                                            <label for=\"optZoneOptionsPrimaryZoneTransferTsigKeyName\" class=\"col-sm-4 control-label\">TSIG Key Name (Optional)</label>\n                                            <div class=\"col-sm-7\">\n                                                <select id=\"optZoneOptionsPrimaryZoneTransferTsigKeyName\" class=\"form-control\"></select>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group\" id=\"divZoneOptionsPrimaryServerValidateZone\">\n                                            <label class=\"col-sm-4 control-label\">Zone Validation</label>\n                                            <div class=\"col-sm-7\">\n                                                <div class=\"checkbox\">\n                                                    <label>\n                                                        <input id=\"chkZoneOptionsValidateZone\" type=\"checkbox\"> Use <a href=\"https://datatracker.ietf.org/doc/rfc8976/\" target=\"_blank\">ZONEMD</a> to Validate Zone\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">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.</div>\n                                                </div>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div id=\"tabPaneZoneOptionsQueryAccess\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 0 6px 0 0; max-height: 450px; margin: 10px 0 0 0; overflow-y: auto; overflow-x: hidden;\">\n                                    <div class=\"well well-sm form-horizontal\">\n                                        <div class=\"form-group\">\n                                            <label class=\"col-sm-3 control-label\">Query Access</label>\n                                            <div class=\"col-sm-8\">\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdQueryAccess\" id=\"rdQueryAccessDeny\" value=\"Deny\">\n                                                        Deny\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Denies everyone from querying the zone by refusing the request.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdQueryAccess\" id=\"rdQueryAccessAllow\" value=\"Allow\">\n                                                        Allow (default)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows everyone to query the zone.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdQueryAccess\" id=\"rdQueryAccessAllowOnlyPrivateNetworks\" value=\"AllowOnlyPrivateNetworks\">\n                                                        Allow Only Private Networks\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows only private networks to query the zone. Any request from a public network will be refused.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divQueryAccessAllowOnlyZoneNameServers\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdQueryAccess\" id=\"rdQueryAccessAllowOnlyZoneNameServers\" value=\"AllowOnlyZoneNameServers\">\n                                                        Allow Only Name Servers In Zone\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows only the name servers with an NS record in the zone to query the zone.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdQueryAccess\" id=\"rdQueryAccessUseSpecifiedNetworkACL\" value=\"UseSpecifiedNetworkACL\">\n                                                        Use Specified Network Access Control List (ACL)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Uses the specified network access control list to allow/deny to query the zone.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdQueryAccess\" id=\"rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\" value=\"AllowZoneNameServersAndUseSpecifiedNetworkACL\">\n                                                        Allow Zone Name Servers And Use Specified Network Access Control List (ACL)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows zone's name servers and uses specified network access control list to allow/deny to query the zone.</div>\n                                                </div>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group\">\n                                            <label for=\"txtQueryAccessNetworkACL\" class=\"col-sm-3 control-label\">Network Access Control List (ACL)</label>\n                                            <div class=\"col-sm-8\">\n                                                <textarea id=\"txtQueryAccessNetworkACL\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another to allow access. Add <code>!</code> character at the start to deny access, e.g. <code>!192.168.10.0/24</code> 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.</div>\n                                            </div>\n                                        </div>\n\n                                        <div>Note! The zone can always be queried from loopback IP addresses and internally by the DNS server irrespective of the Query Access configuration.</div>\n                                    </div>\n                                </div>\n\n                                <div id=\"tabPaneZoneOptionsZoneTransfer\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 0 6px 0 0; max-height: 450px; margin: 10px 0 0 0; overflow-y: auto; overflow-x: hidden;\">\n                                    <div class=\"well well-sm form-horizontal\">\n                                        <div class=\"form-group\">\n                                            <label class=\"col-sm-3 control-label\">Zone Transfer</label>\n                                            <div class=\"col-sm-8\">\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneTransfer\" id=\"rdZoneTransferDeny\" value=\"Deny\">\n                                                        Deny\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Denies everyone from performing a zone transfer.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneTransfer\" id=\"rdZoneTransferAllow\" value=\"Allow\">\n                                                        Allow\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows everyone to perform a zone transfer.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divZoneTransferAllowOnlyZoneNameServers\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneTransfer\" id=\"rdZoneTransferAllowOnlyZoneNameServers\" value=\"AllowOnlyZoneNameServers\">\n                                                        Allow Only Name Servers In Zone\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows only the name servers with an NS record in the zone to perform a zone transfer.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneTransfer\" id=\"rdZoneTransferUseSpecifiedNetworkACL\" value=\"UseSpecifiedNetworkACL\">\n                                                        Use Specified Network Access Control List (ACL)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Uses the specified network access control list to allow/deny to perform a zone transfer.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneTransfer\" id=\"rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\" value=\"AllowZoneNameServersAndUseSpecifiedNetworkACL\">\n                                                        Allow Zone Name Servers And Use Specified Network Access Control List (ACL)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows zone's name servers and uses specified network access control list to allow/deny to perform a zone transfer.</div>\n                                                </div>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group\">\n                                            <label for=\"txtZoneTransferNetworkACL\" class=\"col-sm-3 control-label\">Network Access Control List (ACL)</label>\n                                            <div class=\"col-sm-8\">\n                                                <textarea id=\"txtZoneTransferNetworkACL\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another to allow access. Add <code>!</code> character at the start to deny access, e.g. <code>!192.168.10.0/24</code> 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.</div>\n                                            </div>\n                                        </div>\n\n                                        <div>Note! Zone transfer should be allowed only for trusted name servers to sync their secondary zone.</div>\n                                    </div>\n\n                                    <div class=\"well well-sm form-horizontal\">\n                                        <div class=\"form-group\">\n                                            <label for=\"txtZoneOptionsZoneTransferTsigKeyNames\" class=\"col-sm-3 control-label\">Zone Transfer TSIG Key Names</label>\n                                            <div class=\"col-sm-8\">\n                                                <textarea id=\"txtZoneOptionsZoneTransferTsigKeyNames\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n\n                                                <label for=\"optZoneOptionsQuickTsigKeyNames\" class=\"control-label\">Quick Add</label>\n                                                <select id=\"optZoneOptionsQuickTsigKeyNames\" class=\"form-control\">\n                                                </select>\n                                            </div>\n                                        </div>\n\n                                        <div>Note! TSIG key names must be configured from the Settings before using them here. Entering one or more TSIG key names above will cause the DNS server to authenticate all zone transfer requests. A secondary zone must be configured with one of the above keys to be able to perform a zone transfer.</div>\n                                    </div>\n                                </div>\n\n                                <div id=\"tabPaneZoneOptionsNotify\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 0 6px 0 0; max-height: 450px; margin: 10px 0 0 0; overflow-y: auto; overflow-x: hidden; \">\n                                    <div class=\"well well-sm form-horizontal\">\n                                        <div class=\"form-group\">\n                                            <label class=\"col-sm-3 control-label\">Notify</label>\n                                            <div class=\"col-sm-8\">\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneNotify\" id=\"rdZoneNotifyNone\" value=\"None\">\n                                                        None\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Does not notify any name server when the zone is updated.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divZoneNotifyZoneNameServers\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneNotify\" id=\"rdZoneNotifyZoneNameServers\" value=\"ZoneNameServers\">\n                                                        Name Servers In Zone\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Notifies only the name servers with an NS record in the zone when the zone is updated.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneNotify\" id=\"rdZoneNotifySpecifiedNameServers\" value=\"SpecifiedNameServers\">\n                                                        Specified Name Servers\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Notifies only the specified name servers when the zone is updated.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divZoneNotifyBothZoneAndSpecifiedNameServers\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneNotify\" id=\"rdZoneNotifyBothZoneAndSpecifiedNameServers\" value=\"BothZoneAndSpecifiedNameServers\">\n                                                        Both Zone Name Servers And Specified Name Servers\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Notifies both the zone's name servers and the specified name servers when the zone is updated.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divZoneNotifySeparateNameServersForCatalogAndMemberZones\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdZoneNotify\" id=\"rdZoneNotifySeparateNameServersForCatalogAndMemberZones\" value=\"SeparateNameServersForCatalogAndMemberZones\">\n                                                        Separate Name Servers For Catalog And Member Zones\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Notifies specified name servers for member zone updates and secondary catalog name servers for catalog zone updates.</div>\n                                                </div>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group\">\n                                            <label for=\"txtZoneNotifyNameServers\" class=\"col-sm-3 control-label\">Specified Name Servers</label>\n                                            <div class=\"col-sm-8\">\n                                                <textarea id=\"txtZoneNotifyNameServers\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                <div style=\"padding-top: 5px;\">Enter only the IP addresses of the name servers above.</div>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"divZoneNotifySecondaryCatalogNameServers\" class=\"form-group\">\n                                            <label for=\"txtZoneNotifySecondaryCatalogNameServers\" class=\"col-sm-3 control-label\">Secondary Catalog Name Servers</label>\n                                            <div class=\"col-sm-8\">\n                                                <textarea id=\"txtZoneNotifySecondaryCatalogNameServers\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                <div style=\"padding-top: 5px;\">Enter only the IP addresses of the Secondary Catalog name servers above.</div>\n                                            </div>\n                                        </div>\n\n                                        <div id=\"divZoneNotifyFailedNameServers\" class=\"form-group\" style=\"display: none;\">\n                                            <label class=\"col-sm-3 control-label\">Notify Failed Name Servers</label>\n                                            <div class=\"col-sm-8\">\n                                                <span id=\"lblZoneNotifyFailedNameServers\" class=\"form-control\" style=\"height: auto;\"></span>\n                                            </div>\n                                        </div>\n\n                                        <div>Note! Notification must be enabled to allow other name servers to trigger a zone transfer immediately when the zone is updated.</div>\n                                    </div>\n                                </div>\n\n                                <div id=\"tabPaneZoneOptionsUpdate\" role=\"tabpanel\" class=\"tab-pane\" style=\"padding: 0 6px 0 0; max-height: 450px; margin: 10px 0 0 0; overflow-y: auto; overflow-x: hidden;\">\n                                    <div class=\"well well-sm form-horizontal\">\n                                        <div class=\"form-group\">\n                                            <label class=\"col-sm-3 control-label\">Dynamic Updates</label>\n                                            <div class=\"col-sm-8\">\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdDynamicUpdate\" id=\"rdDynamicUpdateDeny\" value=\"Deny\">\n                                                        Deny (default)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Denies everyone from performing dynamic updates.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdDynamicUpdate\" id=\"rdDynamicUpdateAllow\" value=\"Allow\">\n                                                        Allow\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows everyone to perform dynamic updates.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divDynamicUpdateAllowOnlyZoneNameServers\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdDynamicUpdate\" id=\"rdDynamicUpdateAllowOnlyZoneNameServers\" value=\"AllowOnlyZoneNameServers\">\n                                                        Allow Only Name Servers In Zone\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows only the name servers with an NS record in the zone to perform dynamic updates.</div>\n                                                </div>\n                                                <div class=\"radio\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdDynamicUpdate\" id=\"rdDynamicUpdateUseSpecifiedNetworkACL\" value=\"UseSpecifiedNetworkACL\">\n                                                        Use Specified Network Access Control List (ACL)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Uses the specified network access control list to allow/deny to perform dynamic updates.</div>\n                                                </div>\n                                                <div class=\"radio\" id=\"divDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL\">\n                                                    <label>\n                                                        <input type=\"radio\" name=\"rdDynamicUpdate\" id=\"rdDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL\" value=\"AllowZoneNameServersAndUseSpecifiedNetworkACL\">\n                                                        Allow Zone Name Servers And Use Specified Network Access Control List (ACL)\n                                                    </label>\n                                                    <div style=\"padding-top: 5px; padding-left: 20px;\">Allows zone's name servers and uses specified network access control list to allow/deny to perform dynamic updates.</div>\n                                                </div>\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group\">\n                                            <label for=\"txtDynamicUpdateNetworkACL\" class=\"col-sm-3 control-label\">Network Access Control List (ACL)</label>\n                                            <div class=\"col-sm-8\">\n                                                <textarea id=\"txtDynamicUpdateNetworkACL\" class=\"form-control\" rows=\"5\" spellcheck=\"false\"></textarea>\n                                                <div style=\"padding-top: 5px;\">Enter IP addresses or network addresses one below another to allow access. Add <code>!</code> character at the start to deny access, e.g. <code>!192.168.10.0/24</code> 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.</div>\n                                            </div>\n                                        </div>\n\n                                        <div>Note! Dynamic updates should be allowed only to trusted IP addresses since they will be able to add/delete records in the zone.</div>\n                                        <div style=\"padding-top: 10px;\">Warning! If no security policy is configured in the Primary Zone then access will be provided only based on the options selected here. Thus setting up a security policy in the Primary Zone is highly recommended.</div>\n                                    </div>\n\n                                    <div id=\"divDynamicUpdateSecurityPolicy\" class=\"well well-sm form-horizontal\">\n                                        <p style=\"font-size: 16px; font-weight: bold;\">Security Policy</p>\n\n                                        <table id=\"tableDynamicUpdateSecurityPolicy\" class=\"table table-hover\">\n                                            <thead>\n                                                <tr>\n                                                    <th>TSIG Key Name</th>\n                                                    <th>Domain Name</th>\n                                                    <th>Allowed Record Types</th>\n                                                    <th style=\"width: 84px;\">\n                                                        <button type=\"button\" class=\"btn btn-default\" style=\"padding: 0px 20px;\" onclick=\"addZoneOptionsDynamicUpdatesSecurityPolicyRow();\">Add</button>\n                                                    </th>\n                                                </tr>\n                                            </thead>\n                                            <tbody id=\"tbodyDynamicUpdateSecurityPolicy\"></tbody>\n                                        </table>\n\n                                        <div>Note! Configuring a security policy above will cause the DNS server to authenticate all dynamic update requests. A TSIG key can add/delete records only for the specified domain name and allowed record types. TSIG key names must be configured from the Settings before using them here. Use wildcard domain name to specify all sub domain names. Use a comma separator to specify more than one record type. Use ANY to specify all record types.</div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnSaveZoneOptions\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\" onclick=\"saveZoneOptions(); return false;\">Save</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalDnssecSignZone\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Sign Zone - <span id=\"lblDnssecSignZoneZoneName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divDnssecSignZoneAlert\"></div>\n\n                    <div class=\"form-horizontal\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                        <div class=\"form-group\">\n                            <label class=\"col-sm-4 control-label\">DNSKEY Algorithm</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneAlgorithm\" id=\"rdDnssecSignZoneAlgorithmRsa\" value=\"RSA\">\n                                        RSA\n                                    </label>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneAlgorithm\" id=\"rdDnssecSignZoneAlgorithmEcdsa\" value=\"ECDSA\">\n                                        ECDSA (recommended)\n                                    </label>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneAlgorithm\" id=\"rdDnssecSignZoneAlgorithmEddsa\" value=\"EDDSA\">\n                                        EdDSA\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZoneRsaParameters\">\n                            <div class=\"form-group\">\n                                <label for=\"optDnssecSignZoneRsaHashAlgorithm\" class=\"col-sm-4 control-label\">Hash Algorithm</label>\n                                <div class=\"col-sm-8\">\n                                    <select id=\"optDnssecSignZoneRsaHashAlgorithm\" class=\"form-control\" style=\"width: auto;\">\n                                        <option value=\"MD5\">MD5 (obsolete)</option>\n                                        <option value=\"SHA1\">SHA1 (obsolete)</option>\n                                        <option value=\"SHA256\">SHA256 (default)</option>\n                                        <option>SHA512</option>\n                                    </select>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZoneEcdsaParameters\">\n                            <div class=\"form-group\">\n                                <label for=\"optDnssecSignZoneEcdsaCurve\" class=\"col-sm-4 control-label\">ECDSA Curve</label>\n                                <div class=\"col-sm-8\">\n                                    <select id=\"optDnssecSignZoneEcdsaCurve\" class=\"form-control\" style=\"width: auto;\">\n                                        <option value=\"P256\">P256 (default)</option>\n                                        <option>P384</option>\n                                    </select>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZoneEddsaParameters\">\n                            <div class=\"form-group\">\n                                <label for=\"optDnssecSignZoneEddsaCurve\" class=\"col-sm-4 control-label\">EdDSA Curve</label>\n                                <div class=\"col-sm-8\">\n                                    <select id=\"optDnssecSignZoneEddsaCurve\" class=\"form-control\" style=\"width: auto;\">\n                                        <option value=\"ED25519\">Ed25519 (default)</option>\n                                        <option value=\"ED448\">Ed448</option>\n                                    </select>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label class=\"col-sm-4 control-label\">Key Signing Key (KSK)</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneKskGeneration\" id=\"rdDnssecSignZoneKskGenerationAutomatic\" value=\"Automatic\">\n                                        Automatic Private Key Generation (default)\n                                    </label>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneKskGeneration\" id=\"rdDnssecSignZoneKskGenerationUseSpecified\" value=\"UseSpecified\">\n                                        Use Specified Private Key\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZoneRsaKskKeySize\">\n                            <div class=\"form-group\">\n                                <label for=\"optDnssecSignZoneRsaKskKeySize\" class=\"col-sm-4 control-label\">KSK Size</label>\n                                <div class=\"col-sm-8\">\n                                    <select id=\"optDnssecSignZoneRsaKskKeySize\" class=\"form-control\" style=\"width: auto; display: inline;\">\n                                        <option>1024</option>\n                                        <option>1280</option>\n                                        <option>1536</option>\n                                        <option>2048</option>\n                                        <option>3072</option>\n                                        <option>4096</option>\n                                    </select>\n                                    <span>bits (recommended 2048)</span>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZonePemKskPrivateKey\">\n                            <div class=\"form-group\">\n                                <label for=\"txtDnssecSignZonePemKskPrivateKey\" class=\"col-sm-4 control-label\">KSK Private Key</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtDnssecSignZonePemKskPrivateKey\" class=\"form-control\" rows=\"4\" spellcheck=\"false\" placeholder=\"-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----\"></textarea>\n                                    <div style=\"padding-top: 5px;\">Enter a private key in PEM format.</div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label class=\"col-sm-4 control-label\">Zone Signing Key (ZSK)</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneZskGeneration\" id=\"rdDnssecSignZoneZskGenerationAutomatic\" value=\"Automatic\">\n                                        Automatic Private Key Generation (default)\n                                    </label>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneZskGeneration\" id=\"rdDnssecSignZoneZskGenerationUseSpecified\" value=\"UseSpecified\">\n                                        Use Specified Private Key\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZoneRsaZskKeySize\">\n                            <div class=\"form-group\">\n                                <label for=\"optDnssecSignZoneRsaZskKeySize\" class=\"col-sm-4 control-label\">ZSK Size</label>\n                                <div class=\"col-sm-8\">\n                                    <select id=\"optDnssecSignZoneRsaZskKeySize\" class=\"form-control\" style=\"width: auto; display: inline;\">\n                                        <option>1024</option>\n                                        <option>1280</option>\n                                        <option>1536</option>\n                                        <option>2048</option>\n                                        <option>3072</option>\n                                        <option>4096</option>\n                                    </select>\n                                    <span>bits (default 1280)</span>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZonePemZskPrivateKey\">\n                            <div class=\"form-group\">\n                                <label for=\"txtDnssecSignZonePemZskPrivateKey\" class=\"col-sm-4 control-label\">ZSK Private Key</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtDnssecSignZonePemZskPrivateKey\" class=\"form-control\" rows=\"4\" spellcheck=\"false\" placeholder=\"-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----\"></textarea>\n                                    <div style=\"padding-top: 5px;\">Enter a private key in PEM format.</div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label class=\"col-sm-4 control-label\">Proof of Non-Existence</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneNxProof\" id=\"rdDnssecSignZoneNxProofNSEC\" value=\"NSEC\">\n                                        Next Secure (NSEC) (recommended)\n                                    </label>\n                                    <div style=\"padding-top: 5px; padding-left: 20px;\">\n                                        With NSEC, all the records in your zone can be discovered by anyone using \"zone walking\" technique. NSEC is recommended if your zone does not contain any private/internal records.\n                                    </div>\n                                </div>\n                                <div class=\"radio\">\n                                    <label>\n                                        <input type=\"radio\" name=\"rdDnssecSignZoneNxProof\" id=\"rdDnssecSignZoneNxProofNSEC3\" value=\"NSEC3\">\n                                        Next Secure 3 (NSEC3)\n                                    </label>\n                                    <div style=\"padding-top: 5px; padding-left: 20px;\">\n                                        NSEC3, makes it difficult to perform \"zone walking\" since it uses hashing with a random salt. NSEC3 should be used if your zone contains any private/internal records that you do not wish to be enumerable.\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div id=\"divDnssecSignZoneNSEC3Parameters\">\n                            <div class=\"form-group\">\n                                <label for=\"txtDnssecSignZoneNSEC3Iterations\" class=\"col-sm-4 control-label\">NSEC3 Iterations</label>\n                                <div class=\"col-sm-8\">\n                                    <input id=\"txtDnssecSignZoneNSEC3Iterations\" type=\"number\" class=\"form-control\" placeholder=\"iterations\" style=\"width: 100px; display: inline;\">\n                                    <span>(valid range 0-50, recommended 0)</span>\n                                </div>\n                                <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                    The number of iterations used by NSEC3 for hashing the domain names. It is recommended to use 0 iterations since more iterations will increase computational costs for both the DNS server and resolver while not providing much value against \"zone walking\" [<a href=\"https://www.rfc-editor.org/rfc/rfc9276.html#name-iterations\" target=\"_blank\">RFC 9276</a>].\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtDnssecSignZoneNSEC3SaltLength\" class=\"col-sm-4 control-label\">NSEC3 Salt Length</label>\n                                <div class=\"col-sm-8\">\n                                    <input id=\"txtDnssecSignZoneNSEC3SaltLength\" type=\"number\" class=\"form-control\" placeholder=\"length\" style=\"width: 100px; display: inline;\">\n                                    <span>bytes (valid range 0-32, recommended 0)</span>\n                                </div>\n                                <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                    The number of bytes of random salt to generate to be used with the NSEC3 hash computation. It is recommended to not use salt by setting the length to 0 [<a href=\"https://www.rfc-editor.org/rfc/rfc9276.html#name-salt\" target=\"_blank\">RFC 9276</a>].\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtDnssecSignZoneDnsKeyTtl\" class=\"col-sm-4 control-label\">DNSKEY TTL</label>\n                            <div class=\"col-sm-8\">\n                                <input id=\"txtDnssecSignZoneDnsKeyTtl\" type=\"text\" class=\"form-control\" placeholder=\"ttl\" style=\"width: 100px; display: inline;\">\n                                <span>seconds (default 3600/1h)</span>\n                            </div>\n                            <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                The TTL value to be used for DNSKEY records. A lower value will allow quicker addition or rollover to a new DNS Key at the cost of increased frequency of DNSKEY queries by resolvers.\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtDnssecSignZoneZskAutoRollover\" class=\"col-sm-4 control-label\">ZSK Automatic Rollover</label>\n                            <div class=\"col-sm-8\">\n                                <input id=\"txtDnssecSignZoneZskAutoRollover\" type=\"number\" class=\"form-control\" placeholder=\"days\" style=\"width: 100px; display: inline;\">\n                                <span>days (valid range 0-365; default 30; set 0 to disable)</span>\n                            </div>\n                            <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                The frequency at which the DNS server must automatically rollover the Zone Signing Key (ZSK).\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <div class=\"pull-left\" style=\"padding: 6px 0;\">\n                        <a href=\"https://blog.technitium.com/2022/07/how-to-secure-your-domain-name-with-.html\" target=\"_blank\">Help: How To Secure Your Domain Name With DNSSEC</a>\n                    </div>\n                    <div class=\"pull-right\">\n                        <button id=\"btnDnssecSignZone\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Signing...\" onclick=\"signPrimaryZone(); return false;\">Sign Zone</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalDnssecUnsignZone\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Unsign Zone - <span id=\"lblDnssecUnsignZoneZoneName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divDnssecUnsignZoneAlert\"></div>\n\n                    <p><b>Warning!</b> Unsigning the zone without removing all DS records from its parent zone will cause DNSSEC validating recursive resolvers to mark the zone as <b>bogus</b> and fail to resolve it.</p>\n\n                    <p><b>Warning!</b> Make sure that you have removed all of the DS records from the parent zone and sufficient time has passed before unsigning this zone. You MUST wait for at least the number of seconds specified by the DS record's TTL value to elapse before unsigning the zone to ensure that all recursive resolvers would have expired the DS records from its cache. For example, if you have DS records at the parent zone with TTL value set to 86400 then you must wait for 86400 seconds (24 hours) to pass after you delete the DS records from the parent zone. Once you have ensured that you have waited for the appropriate time then you can unsign the zone safely.</p>\n\n                    <p><b>Note!</b> You can find out the TTL value of DS records for your zone by querying for DS records using the DNS Client tab.</p>\n\n                    <p><b>Warning!</b> Unsigning the zone will permanently delete all of the private keys associated with it. Consider taking a backup before proceeding.</p>\n\n                    <p>&nbsp;</p>\n                    <p>Are you sure you want to proceed to unsign the zone now?</p>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnDnssecUnsignZone\" type=\"submit\" class=\"btn btn-danger\" data-loading-text=\"Unsigning...\" onclick=\"unsignPrimaryZone(); return false;\">Unsign Zone</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalDnssecViewDs\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\" style=\"width: 940px;\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">View DS Info - <span id=\"lblDnssecViewDsZoneName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divDnssecViewDsAlert\"></div>\n\n                    <div id=\"divDnssecViewDsLoader\" style=\"height: 500px;\"></div>\n\n                    <div id=\"divDnssecViewDs\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                        <p>\n                            Use the DNS Key data given below to add DS records for your zone. Before adding the DS records, you must read and understand the following points:\n                            <ul>\n                                <li>The Key State for a newly published DNS Key must be <code>Ready</code> before you can add a DS record for it. Adding DS record for a DNS Key with <code>Published</code> Key State may cause DNSSEC validation to fail for some DNS resolvers. A \"ready by\" timestamp is displayed to let you know when a DS record can be added for a DNS Key that is not \"Ready\" yet.</li>\n                                <li>You should add only one DS record for each Key Tag. That is, do not create multiple DS records for each Digest Type, instead use the Digest Type that is supported by your Domain Registrar.</li>\n                                <li>Use the provided Public Key if the Domain Registrar requires it instead of the Digest.</li>\n                                <li>When doing a Key Signing Key (KSK) rollover, you can immediately delete the old DS record after adding the new DS record.</li>\n                            </ul>\n                        </p>\n\n                        <table class=\"table well well-sm\">\n                            <thead>\n                                <tr>\n                                    <th><a>Key Tag</a></th>\n                                    <th><a>Key State</a></th>\n                                    <th><a>Algorithm</a></th>\n                                    <th><a>Digest Type</a></th>\n                                    <th><a>Digest</a></th>\n                                </tr>\n                            </thead>\n                            <tbody id=\"tableDnssecViewDsBody\">\n                            </tbody>\n                        </table>\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalDnssecProperties\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\" style=\"width: 940px;\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">DNSSEC Properties - <span id=\"lblDnssecPropertiesZoneName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divDnssecPropertiesAlert\"></div>\n\n                    <div id=\"divDnssecPropertiesLoader\" style=\"height: 500px;\"></div>\n\n                    <div id=\"divDnssecProperties\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                        <div class=\"well well-sm form-horizontal\">\n                            <table class=\"table table-hover\" style=\"margin-bottom: 10px;\">\n                                <thead>\n                                    <tr>\n                                        <th><a href=\"#\" onclick=\"sortTable('tableDnssecPropertiesPrivateKeysBody', 0); return false;\">Key Tag</a></th>\n                                        <th><a href=\"#\" onclick=\"sortTable('tableDnssecPropertiesPrivateKeysBody', 1); return false;\">Key Type</a></th>\n                                        <th><a href=\"#\" onclick=\"sortTable('tableDnssecPropertiesPrivateKeysBody', 2); return false;\">Algorithm</a></th>\n                                        <th><a href=\"#\" onclick=\"sortTable('tableDnssecPropertiesPrivateKeysBody', 3); return false;\">State</a></th>\n                                        <th><a href=\"#\" onclick=\"sortTable('tableDnssecPropertiesPrivateKeysBody', 4); return false;\">State Changed</a></th>\n                                        <th><a href=\"#\" onclick=\"sortTable('tableDnssecPropertiesPrivateKeysBody', 5); return false;\">Rollover (days)</a></th>\n                                        <th style=\"width: 36px;\"></th>\n                                    </tr>\n                                </thead>\n                                <tbody id=\"tableDnssecPropertiesPrivateKeysBody\">\n                                </tbody>\n                            </table>\n\n                            <div>\n                                <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 0; width: 120px;\" data-toggle=\"collapse\" data-target=\"#divDnssecPropertiesAddKey\" aria-expanded=\"false\" aria-controls=\"divDnssecPropertiesAddKey\">Add Private Key</button>\n                                <button id=\"btnDnssecPropertiesPublishKeys\" type=\"button\" class=\"btn btn-warning\" style=\"padding: 2px 0; width: 120px;\" data-loading-text=\"Publishing...\" onclick=\"publishAllDnssecPrivateKeys(this);\">Publish All Keys</button>\n                            </div>\n\n                            <div id=\"divDnssecPropertiesAddKey\" class=\"collapse\">\n                                <div class=\"panel panel-default\" style=\"margin-bottom: 0px; margin-top: 10px; padding-top: 15px;\">\n                                    <div class=\"form-group\">\n                                        <label for=\"optDnssecPropertiesAddKeyKeyType\" class=\"col-sm-4 control-label\">Key Type</label>\n                                        <div class=\"col-sm-8\">\n                                            <select id=\"optDnssecPropertiesAddKeyKeyType\" class=\"form-control\" style=\"width: auto;\">\n                                                <option value=\"KeySigningKey\">Key Signing Key (KSK)</option>\n                                                <option value=\"ZoneSigningKey\">Zone Signing Key (ZSK)</option>\n                                            </select>\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group\">\n                                        <label for=\"optDnssecPropertiesAddKeyAlgorithm\" class=\"col-sm-4 control-label\">Algorithm</label>\n                                        <div class=\"col-sm-8\">\n                                            <select id=\"optDnssecPropertiesAddKeyAlgorithm\" class=\"form-control\" style=\"width: auto;\">\n                                                <option>RSA</option>\n                                                <option value=\"ECDSA\">ECDSA (recommended)</option>\n                                                <option value=\"EDDSA\">EdDSA</option>\n                                            </select>\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divDnssecPropertiesAddKeyRsaParameters\">\n                                        <div class=\"form-group\">\n                                            <label for=\"optDnssecPropertiesAddKeyRsaHashAlgorithm\" class=\"col-sm-4 control-label\">Hash Algorithm</label>\n                                            <div class=\"col-sm-8\">\n                                                <select id=\"optDnssecPropertiesAddKeyRsaHashAlgorithm\" class=\"form-control\" style=\"width: auto;\">\n                                                    <option value=\"MD5\">MD5 (obsolete)</option>\n                                                    <option value=\"SHA1\">SHA1 (obsolete)</option>\n                                                    <option value=\"SHA256\">SHA256 (default)</option>\n                                                    <option>SHA512</option>\n                                                </select>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divDnssecPropertiesAddKeyEcdsaParameters\">\n                                        <div class=\"form-group\">\n                                            <label for=\"optDnssecPropertiesAddKeyEcdsaCurve\" class=\"col-sm-4 control-label\">ECDSA Curve</label>\n                                            <div class=\"col-sm-8\">\n                                                <select id=\"optDnssecPropertiesAddKeyEcdsaCurve\" class=\"form-control\" style=\"width: auto;\">\n                                                    <option value=\"P256\">P256 (default)</option>\n                                                    <option>P384</option>\n                                                </select>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divDnssecPropertiesAddKeyEddsaParameters\">\n                                        <div class=\"form-group\">\n                                            <label for=\"optDnssecPropertiesAddKeyEddsaCurve\" class=\"col-sm-4 control-label\">EdDSA Curve</label>\n                                            <div class=\"col-sm-8\">\n                                                <select id=\"optDnssecPropertiesAddKeyEddsaCurve\" class=\"form-control\" style=\"width: auto;\">\n                                                    <option value=\"ED25519\">Ed25519 (default)</option>\n                                                    <option value=\"ED448\">Ed448</option>\n                                                </select>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group\">\n                                        <label class=\"col-sm-4 control-label\">Key Generation</label>\n                                        <div class=\"col-sm-8\">\n                                            <div class=\"radio\">\n                                                <label>\n                                                    <input type=\"radio\" name=\"rdDnssecPropertiesKeyGeneration\" id=\"rdDnssecPropertiesKeyGenerationAutomatic\" value=\"Automatic\">\n                                                    Automatic Private Key Generation (default)\n                                                </label>\n                                            </div>\n                                            <div class=\"radio\">\n                                                <label>\n                                                    <input type=\"radio\" name=\"rdDnssecPropertiesKeyGeneration\" id=\"rdDnssecPropertiesKeyGenerationUseSpecified\" value=\"UseSpecified\">\n                                                    Use Specified Private Key\n                                                </label>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divDnssecPropertiesAddKeyRsaKeySize\">\n                                        <div class=\"form-group\">\n                                            <label for=\"optDnssecPropertiesAddKeyRsaKeySize\" class=\"col-sm-4 control-label\">Key Size</label>\n                                            <div class=\"col-sm-8\">\n                                                <select id=\"optDnssecPropertiesAddKeyRsaKeySize\" class=\"form-control\" style=\"width: auto; display: inline;\">\n                                                    <option>1024</option>\n                                                    <option>1280</option>\n                                                    <option>1536</option>\n                                                    <option>2048</option>\n                                                    <option>3072</option>\n                                                    <option>4096</option>\n                                                </select>\n                                                <span>bits</span>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divDnssecPropertiesPemPrivateKey\">\n                                        <div class=\"form-group\">\n                                            <label for=\"txtDnssecPropertiesPemPrivateKey\" class=\"col-sm-4 control-label\">Private Key</label>\n                                            <div class=\"col-sm-7\">\n                                                <textarea id=\"txtDnssecPropertiesPemPrivateKey\" class=\"form-control\" rows=\"4\" spellcheck=\"false\" placeholder=\"-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----\"></textarea>\n                                                <div style=\"padding-top: 5px;\">Enter a private key in PEM format.</div>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div id=\"divDnssecPropertiesAddKeyAutomaticRollover\" class=\"form-group\">\n                                        <label for=\"txtDnssecPropertiesAddKeyAutomaticRollover\" class=\"col-sm-4 control-label\">Automatic Key Rollover</label>\n                                        <div class=\"col-sm-8\">\n                                            <div>\n                                                <input id=\"txtDnssecPropertiesAddKeyAutomaticRollover\" type=\"number\" class=\"form-control\" placeholder=\"days\" style=\"width: 100px; display: inline;\">\n                                                <span>days (valid range 0-365; default 30; set 0 to disable)</span>\n                                            </div>\n                                        </div>\n                                        <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                            The frequency at which the DNS server must automatically rollover the key.\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group\">\n                                        <div class=\"col-sm-offset-4 col-sm-8\">\n                                            <button type=\"button\" class=\"btn btn-primary\" style=\"padding: 2px 10px;\" data-loading-text=\"Generating...\" onclick=\"addDnssecPrivateKey(this);\">Add Key</button>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div id=\"divDnssecPropertiesNoteReadyBy\" style=\"margin-top: 10px; display: none;\">\n                                Note! The Key State for a newly published Key Signing Key (KSK) must be <code>Ready</code> before you can add a DS record for it. Use the <b>View DS Info</b> option for more details on adding DS record.\n                            </div>\n                            <div id=\"divDnssecPropertiesNoteActiveBy\" style=\"margin-top: 10px; display: none;\">\n                                Note! The Key State for a Key Signing Key (KSK) will automatically switch from <code>Ready</code> to <code>Active</code> once you have added DS record for it and the DNS server is able to detect it. Use the <b>View DS Info</b> option for more details on adding DS record.\n                            </div>\n                            <div id=\"divDnssecPropertiesNoteRetiredRevoked\" style=\"margin-top: 10px; display: none;\">\n                                Note! The keys with <code>Retired</code> or <code>Revoked</code> Key State will be automatically unpublished and removed when its safe to do so.\n                            </div>\n                        </div>\n\n                        <div class=\"well well-sm form-horizontal\">\n                            <div class=\"form-group\">\n                                <label class=\"col-sm-4 control-label\">Proof of Non-Existence</label>\n                                <div class=\"col-sm-8\">\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdDnssecPropertiesNxProof\" id=\"rdDnssecPropertiesNxProofNSEC\" value=\"NSEC\">\n                                            Next Secure (NSEC) (recommended)\n                                        </label>\n                                        <div style=\"padding-top: 5px; padding-left: 20px;\">\n                                            With NSEC, all the records in your zone can be discovered by anyone using \"zone walking\" technique. NSEC is recommended if your zone does not contain any private/internal records.\n                                        </div>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdDnssecPropertiesNxProof\" id=\"rdDnssecPropertiesNxProofNSEC3\" value=\"NSEC3\">\n                                            Next Secure 3 (NSEC3)\n                                        </label>\n                                        <div style=\"padding-top: 5px; padding-left: 20px;\">\n                                            NSEC3, makes it difficult to perform \"zone walking\" since it uses hashing with a random salt. NSEC3 should be used if your zone contains any private/internal records that you do not wish to be enumerable.\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div id=\"divDnssecPropertiesNSEC3Parameters\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtDnssecPropertiesNSEC3Iterations\" class=\"col-sm-4 control-label\">NSEC3 Iterations</label>\n                                    <div class=\"col-sm-8\">\n                                        <input id=\"txtDnssecPropertiesNSEC3Iterations\" type=\"number\" class=\"form-control\" placeholder=\"iterations\" style=\"width: 100px; display: inline;\">\n                                        <span>(valid range 0-50, recommended 0)</span>\n                                    </div>\n                                    <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                        The number of iterations used by NSEC3 for hashing the domain names. It is recommended to use 0 iterations since more iterations will increase computational costs for both the DNS server and resolver while not providing much value against \"zone walking\" [<a href=\"https://www.rfc-editor.org/rfc/rfc9276.html#name-iterations\" target=\"_blank\">RFC 9276</a>].\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtDnssecPropertiesNSEC3SaltLength\" class=\"col-sm-4 control-label\">NSEC3 Salt Length</label>\n                                    <div class=\"col-sm-8\">\n                                        <input id=\"txtDnssecPropertiesNSEC3SaltLength\" type=\"number\" class=\"form-control\" placeholder=\"length\" style=\"width: 100px; display: inline;\">\n                                        <span>bytes (valid range 0-32, recommended 0)</span>\n                                    </div>\n                                    <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                        The number of bytes of random salt to generate to be used with the NSEC3 hash computation. It is recommended to not use salt by setting the length to 0 [<a href=\"https://www.rfc-editor.org/rfc/rfc9276.html#name-salt\" target=\"_blank\">RFC 9276</a>].\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\" style=\"margin-bottom: 5px;\">\n                                <div class=\"col-sm-offset-4 col-sm-8\">\n                                    <button id=\"btnDnssecPropertiesChangeNxProof\" type=\"button\" class=\"btn btn-warning\" style=\"padding: 2px 0; width: 100px;\" data-loading-text=\"Changing...\" onclick=\"changeDnssecNxProof(this);\">Change</button>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"well well-sm form-horizontal\">\n                            <div class=\"form-group\" style=\"margin-bottom: 5px;\">\n                                <label for=\"txtDnssecPropertiesDnsKeyTtl\" class=\"col-sm-4 control-label\">DNSKEY TTL</label>\n                                <div class=\"col-sm-8\">\n                                    <div>\n                                        <input id=\"txtDnssecPropertiesDnsKeyTtl\" type=\"text\" class=\"form-control\" placeholder=\"ttl\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds (default 3600/1h)</span>\n                                    </div>\n                                    <div style=\"margin-top: 10px;\">\n                                        <button type=\"button\" class=\"btn btn-default\" style=\"padding: 2px 0; width: 100px;\" data-loading-text=\"Updating...\" onclick=\"updateDnssecDnsKeyTtl(this);\">Update TTL</button>\n                                    </div>\n                                </div>\n                                <div class=\"col-sm-offset-4 col-sm-8\" style=\"padding-top: 5px;\">\n                                    The TTL value to be used for DNSKEY records. A lower value will allow quicker addition or rollover to a new DNS Key at the cost of increased frequency of DNSKEY queries by resolvers.\n                                </div>\n                            </div>\n                            <div style=\"margin-top: 10px;\">\n                                Warning! You MUST wait for at least the number of seconds specified by the the old TTL value to elapse before making any changes to the DNS keys above to ensure that all recursive resolvers would have expired the DNSKEY records from its cache. For example, if the old TTL value was 3600, then you must wait for 3600 seconds (1 hour) to pass before making any changes to the DNS keys.\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <div id=\"modalImportAllowedZones\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Import Allowed Zones</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divImportAllowedZonesAlert\"></div>\n\n                    <p>Enter domain names one below other to import into Allowed Zone:</p>\n\n                    <div class=\"form-group\">\n                        <label for=\"txtImportAllowedZones\" class=\"control-label\">Allowed Zones</label>\n                        <textarea id=\"txtImportAllowedZones\" class=\"form-control\" rows=\"15\" spellcheck=\"false\"></textarea>\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnImportAllowedZones\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Importing...\" onclick=\"importAllowedZones(); return false;\">Import</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalImportBlockedZones\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Import Blocked Zones</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divImportBlockedZonesAlert\"></div>\n\n                    <p>Enter domain names one below other to import into blocked zone:</p>\n\n                    <div class=\"form-group\">\n                        <label for=\"txtImportBlockedZones\" class=\"control-label\">Blocked Zones</label>\n                        <textarea id=\"txtImportBlockedZones\" class=\"form-control\" rows=\"15\" spellcheck=\"false\"></textarea>\n                    </div>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnImportBlockedZones\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Importing...\" onclick=\"importBlockedZones(); return false;\">Import</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <div id=\"modalStoreApps\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 800px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">DNS App Store</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divStoreAppsAlert\"></div>\n\n                        <div id=\"divStoreAppsLoader\" style=\"height: 500px;\"></div>\n\n                        <div id=\"divStoreApps\" style=\"max-height: 500px; overflow-y: auto; display: none;\">\n                            <table class=\"table table-hover\">\n                                <thead>\n                                    <tr>\n                                        <th style=\"min-width: 120px;\"><a href=\"#\" onclick=\"sortTable('tableStoreAppsBody', 0); return false;\">Store Apps</a></th>\n                                        <th style=\"width: 96px;\"></th>\n                                    </tr>\n                                </thead>\n                                <tbody id=\"tableStoreAppsBody\">\n                                </tbody>\n                                <tfoot id=\"tableStoreAppsFooter\">\n                                    <tr><td colspan=\"3\"><b>Total Apps: 0</b></td></tr>\n                                </tfoot>\n                            </table>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalInstallApp\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Install App</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divInstallAppAlert\"></div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtInstallApp\" class=\"col-sm-4 control-label\">App Name</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtInstallApp\" type=\"text\" class=\"form-control\">\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"fileAppZip\" class=\"col-sm-4 control-label\">App Zip File</label>\n                            <div class=\"col-sm-7\">\n                                <input type=\"file\" class=\"form-control\" id=\"fileAppZip\">\n                            </div>\n                        </div>\n\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnInstallApp\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Installing...\" onclick=\"installApp(); return false;\">Install</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalUpdateApp\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Update App</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divUpdateAppAlert\"></div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtUpdateApp\" class=\"col-sm-4 control-label\">App Name</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtUpdateApp\" type=\"text\" class=\"form-control\" disabled>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"fileUpdateAppZip\" class=\"col-sm-4 control-label\">App Zip File</label>\n                            <div class=\"col-sm-7\">\n                                <input type=\"file\" class=\"form-control\" id=\"fileUpdateAppZip\">\n                            </div>\n                        </div>\n\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnUpdateApp\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Updating...\" onclick=\"updateApp(); return false;\">Update</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalAppConfig\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">App Config - <span id=\"lblAppConfigName\"></span></h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divAppConfigAlert\"></div>\n\n                    <p>Edit the <code>dnsApp.config</code> config file below as required by the DNS application.</p>\n\n                    <div class=\"form-group\">\n                        <label for=\"txtAppConfig\" class=\"control-label\">Config File</label>\n                        <textarea id=\"txtAppConfig\" class=\"form-control\" rows=\"15\" spellcheck=\"false\"></textarea>\n                    </div>\n\n                    <p>Note: The app will reload the config automatically after you save it.</p>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnAppConfig\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\" onclick=\"saveAppConfig(); return false;\">Save</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <div id=\"modalBackupSettings\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Backup Settings</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divBackupSettingsAlert\"></div>\n\n                    <p>The backup process will create a zip file for the items selected below:</p>\n\n                    <div class=\"form-horizontal\">\n                        <div class=\"form-group\">\n                            <div style=\"padding-left: 40px;\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupAuthConfig\" type=\"checkbox\" checked> Authentication Config File (auth.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupClusterConfig\" type=\"checkbox\" checked> Cluster Config File (cluster.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupWebServiceConfig\" type=\"checkbox\" checked> Web Service Config And Certificate File (webservice.config, *.pfx &amp; *.p12)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupDnsConfig\" type=\"checkbox\" checked> DNS Config And Certificate File (dns.config, *.pfx &amp; *.p12)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupLogConfig\" type=\"checkbox\" checked> Log Config File (log.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupZones\" type=\"checkbox\" checked> DNS Zone Files (*.zone)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupAllowedZones\" type=\"checkbox\" checked> Allowed Zones File (allowed.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupBlockedZones\" type=\"checkbox\" checked> Blocked Zones File (blocked.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupBlockLists\" type=\"checkbox\" checked> Block List Config And Cache Files (blocklist.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupApps\" type=\"checkbox\" checked> DNS Apps\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupScopes\" type=\"checkbox\" checked> DHCP Scope Files (*.scope)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupStats\" type=\"checkbox\" checked> Dashboard Stats Files (*.stat, *.dstat)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkBackupLogs\" type=\"checkbox\"> Log Files (*.log)\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n\n                    <p><b>Note!</b> The Web Service or Optional Protocols TLS certificate (.pfx or .p12) files will be included in the backup only if they exist within the DNS server's config folder.</p>\n\n                    <p><b>Note!</b> It may take several minutes to generate the backup zip file if log files are selected to be backed up which will depend on the size of the log files on the disk.</p>\n\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Backup\" onclick=\"backupSettings(); return false;\">Backup</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"modalRestoreSettings\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Restore Settings</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divRestoreSettingsAlert\"></div>\n\n                    <div class=\"form-horizontal\">\n                        <div class=\"form-group\">\n                            <label for=\"fileBackupZip\" class=\"col-sm-3 control-label\">Backup Zip File</label>\n                            <div class=\"col-sm-6\">\n                                <input type=\"file\" class=\"form-control\" id=\"fileBackupZip\">\n                            </div>\n                        </div>\n\n                        <p>The restore process will restore all the selected items from the backup zip file:</p>\n\n                        <div class=\"form-group\">\n                            <div style=\"padding-left: 40px;\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreAuthConfig\" type=\"checkbox\" checked> Authentication Config File (auth.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreClusterConfig\" type=\"checkbox\" checked> Cluster Config File (cluster.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreWebServiceConfig\" type=\"checkbox\" checked> Web Service Config And Certificate File (webservice.config, *.pfx &amp; *.p12)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreDnsConfig\" type=\"checkbox\" checked> DNS Config And Certificate File (dns.config, *.pfx &amp; *.p12)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreLogConfig\" type=\"checkbox\" checked> Log Config File (log.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreZones\" type=\"checkbox\" checked> DNS Zone Files (*.zone)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreAllowedZones\" type=\"checkbox\" checked> Allowed Zones File (allowed.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreBlockedZones\" type=\"checkbox\" checked> Blocked Zones File (blocked.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreBlockLists\" type=\"checkbox\" checked> Block List Config And Cache Files (blocklist.config)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreApps\" type=\"checkbox\" checked> DNS Apps\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreScopes\" type=\"checkbox\" checked> DHCP Scope Files (*.scope)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreStats\" type=\"checkbox\" checked> Dashboard Stats Files (*.stat, *.dstat)\n                                    </label>\n                                </div>\n\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRestoreLogs\" type=\"checkbox\"> Log Files (*.log)\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n\n                        <p>Restore options:</p>\n\n                        <div class=\"form-group\">\n                            <div style=\"padding-left: 40px;\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkDeleteExistingFiles\" type=\"checkbox\" checked> Delete Existing Files For Selected Items\n                                    </label>\n                                </div>\n                            </div>\n                        </div>\n\n                    </div>\n\n                    <p><b>Warning!</b> The restore process will overwrite existing config files on disk for above selected items and reload new settings including user accounts from the backup. The current logged in user account and current session will be added automatically.</p>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnRestoreSettings\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Restoring...\" onclick=\"restoreSettings(); return false;\">Restore</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <div id=\"modalTopStats\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 id=\"lblTopStatsTitle\" class=\"modal-title\">Top Stats</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divTopStatsAlert\"></div>\n\n                    <div id=\"divTopStatsLoader\" style=\"height: 500px;\"></div>\n\n                    <div id=\"divTopStatsData\" style=\"max-height: 500px; overflow-y: auto;\">\n                        <table id=\"tableTopStatsClients\" class=\"table table-hover\">\n                            <thead>\n                                <tr>\n                                    <th>Client</th>\n                                    <th>Queries</th>\n                                    <th style=\"width: 36px;\"></th>\n                                </tr>\n                            </thead>\n                            <tbody id=\"tbodyTopStatsClients\">\n                            </tbody>\n                            <tfoot>\n                                <tr><th colspan=\"3\" id=\"tfootTopStatsClients\"></th></tr>\n                            </tfoot>\n                        </table>\n\n                        <table id=\"tableTopStatsDomains\" class=\"table table-hover\">\n                            <thead>\n                                <tr>\n                                    <th>Domain</th>\n                                    <th>Hits</th>\n                                    <th style=\"width: 36px;\"></th>\n                                </tr>\n                            </thead>\n                            <tbody id=\"tbodyTopStatsDomains\">\n                            </tbody>\n                            <tfoot>\n                                <tr><th colspan=\"3\" id=\"tfootTopStatsDomains\"></th></tr>\n                            </tfoot>\n                        </table>\n\n                        <table id=\"tableTopStatsBlockedDomains\" class=\"table table-hover\">\n                            <thead>\n                                <tr>\n                                    <th>Domain</th>\n                                    <th>Hits</th>\n                                    <th style=\"width: 36px;\"></th>\n                                </tr>\n                            </thead>\n                            <tbody id=\"tbodyTopStatsBlockedDomains\">\n                            </tbody>\n                            <tfoot>\n                                <tr><th colspan=\"3\" id=\"tfootTopStatsBlockedDomains\"></th></tr>\n                            </tfoot>\n                        </table>\n                    </div>\n\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <div id=\"modalDhcpRemoveLease\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <h4 class=\"modal-title\">Remove Lease?</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <div id=\"divDhcpRemoveLeaseAlert\"></div>\n\n                    <p><b>Warning!</b> Removing a DHCP lease from the server side will NOT remove the allocated IP address from the client side. Make sure that the client assigned this lease is not connected to the network before proceeding.</p>\n                    <p><b>Warning!</b> Removing a DHCP lease may cause IP address conflict if the DHCP server assigns the same IP address to a new client while the old client is still connected to the network.</p>\n                    <p>It is not recommended to remove a DHCP lease when the client is still connected or may connect back later to the network before the lease expires. Use this option only as a last resort.</p>\n                    <p>\n                        Follow the recommendations below to avoid such a case that requires removing a DHCP lease:\n                        <ul>\n                            <li>Use a shorter lease time such that a dynamically allocated lease expires quickly when the client exits the network.</li>\n                            <li>Use Exclusions to exclude IP address ranges from being dynamically allocated that you plan to assign manually to some of the devices on the network.</li>\n                            <li>Use Exclusions to make sure that you have unallocated addresses available in the DHCP scope to be assigned as reserved leases in future.</li>\n                            <li>Rely less on the assigned IP addresses by configuring a domain name for the DHCP scope and accessing all the devices using their domain names.</li>\n                        </ul>\n                    </p>\n                    <p>&nbsp;</p>\n                    <p>Are you sure you want to remove the DHCP lease now?</p>\n                </div>\n                <div class=\"modal-footer\">\n                    <button id=\"btnRemoveDhcpLease\" type=\"button\" class=\"btn btn-danger\" data-loading-text=\"Removing...\">Remove</button>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <div id=\"modalAddUser\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Add User</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divAddUserAlert\"></div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtAddUserDisplayName\" class=\"col-sm-4 control-label\">Display Name</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtAddUserDisplayName\" type=\"text\" class=\"form-control\" placeholder=\"display name\" maxlength=\"255\">\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtAddUserUsername\" class=\"col-sm-4 control-label\">Username</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtAddUserUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\" maxlength=\"255\">\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtAddUserPassword\" class=\"col-sm-4 control-label\">Password</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtAddUserPassword\" type=\"password\" class=\"form-control\" placeholder=\"password\" maxlength=\"255\">\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtAddUserConfirmPassword\" class=\"col-sm-4 control-label\">Confirm Password</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtAddUserConfirmPassword\" type=\"password\" class=\"form-control\" placeholder=\"confirm password\" maxlength=\"255\">\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnAddUser\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Adding...\" onclick=\"addUser(this); return false;\">Add</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalUserDetails\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 940px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">User Details</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divUserDetailsAlert\"></div>\n\n                        <div id=\"divUserDetailsLoader\" style=\"height: 500px;\"></div>\n\n                        <div id=\"divUserDetailsViewer\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <div class=\"form-group\">\n                                <label for=\"txtUserDetailsDisplayName\" class=\"col-sm-4 control-label\">Display Name</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtUserDetailsDisplayName\" type=\"text\" class=\"form-control\" placeholder=\"display name\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtUserDetailsUsername\" class=\"col-sm-4 control-label\">Username</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtUserDetailsUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"lblUserDetails2FAStatus\" class=\"col-sm-4 control-label\">2FA Status</label>\n                                <div class=\"col-sm-7\">\n                                    <div id=\"lblUserDetails2FAStatus\" style=\"padding: 6px 0; font-weight: bold;\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <div class=\"col-sm-offset-4 col-sm-7\">\n                                    <div class=\"checkbox\" style=\"padding: 0px;\">\n                                        <label>\n                                            <input id=\"chkUserDetailsDisableAccount\" type=\"checkbox\"> Disable User Account\n                                        </label>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtUserDetailsSessionTimeout\" class=\"col-sm-4 control-label\">Session Timeout</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtUserDetailsSessionTimeout\" type=\"number\" class=\"form-control\" placeholder=\"1800\" style=\"width: 100px; display: inline;\">\n                                    <span>seconds (valid range 0-604800; default 1800; set 0 to disable)</span>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtUserDetailsMemberOf\" class=\"col-sm-4 control-label\">Member Of</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtUserDetailsMemberOf\" class=\"form-control\" rows=\"5\"></textarea>\n                                    <label class=\"control-label\" for=\"optUserDetailsGroupList\">Add Group</label>\n                                    <select id=\"optUserDetailsGroupList\" class=\"form-control\"></select>\n                                </div>\n                            </div>\n\n                            <div class=\"well well-sm\" style=\"background-color: #fbfbfb;\">\n                                <p style=\"font-size: 16px; font-weight: bold;\">Active Sessions</p>\n                                <table id=\"tableUserDetailsActiveSessions\" class=\"table table-hover\" style=\"margin-bottom: 0px;\">\n                                    <thead>\n                                        <tr>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyUserDetailsActiveSessions', 0); return false;\">Session</a></th>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyUserDetailsActiveSessions', 1); return false;\">Last Seen</a></th>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyUserDetailsActiveSessions', 2); return false;\">Remote Address</a></th>\n                                            <th><a href=\"#\" onclick=\"sortTable('tbodyUserDetailsActiveSessions', 3); return false;\">User Agent</a></th>\n                                            <th style=\"width: 36px;\"></th>\n                                        </tr>\n                                    </thead>\n                                    <tbody id=\"tbodyUserDetailsActiveSessions\">\n                                    </tbody>\n                                    <tfoot>\n                                        <tr><th colspan=\"5\" id=\"tfootUserDetailsActiveSessions\"></th></tr>\n                                    </tfoot>\n                                </table>\n                            </div>\n\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnUserDetailsSave\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\" onclick=\"saveUserDetails(this); return false;\">Save</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalAddGroup\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Add Group</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divAddGroupAlert\"></div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtAddGroupName\" class=\"col-sm-4 control-label\">Name</label>\n                            <div class=\"col-sm-7\">\n                                <input id=\"txtAddGroupName\" type=\"text\" class=\"form-control\" placeholder=\"group name\" maxlength=\"255\">\n                            </div>\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"txtAddGroupDescription\" class=\"col-sm-4 control-label\">Description</label>\n                            <div class=\"col-sm-7\">\n                                <textarea id=\"txtAddGroupDescription\" class=\"form-control\" rows=\"5\" maxlength=\"255\"></textarea>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnAddGroup\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Adding...\" onclick=\"addGroup(this); return false;\">Add</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalGroupDetails\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Group Details</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divGroupDetailsAlert\"></div>\n\n                        <div id=\"divGroupDetailsLoader\" style=\"height: 500px;\"></div>\n\n                        <div id=\"divGroupDetailsViewer\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <div class=\"form-group\">\n                                <label for=\"txtGroupDetailsName\" class=\"col-sm-4 control-label\">Name</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtGroupDetailsName\" type=\"text\" class=\"form-control\" placeholder=\"group name\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtGroupDetailsDescription\" class=\"col-sm-4 control-label\">Description</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtGroupDetailsDescription\" class=\"form-control\" rows=\"3\" maxlength=\"255\"></textarea>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtGroupDetailsMembers\" class=\"col-sm-4 control-label\">Members</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtGroupDetailsMembers\" class=\"form-control\" rows=\"7\"></textarea>\n                                    <label class=\"control-label\" for=\"optGroupDetailsUserList\">Add User</label>\n                                    <select id=\"optGroupDetailsUserList\" class=\"form-control\"></select>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnGroupDetailsSave\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\" onclick=\"saveGroupDetails(this); return false;\">Save</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalEditPermissions\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Edit Permissions - <span id=\"lblEditPermissionsName\"></span></h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divEditPermissionsAlert\"></div>\n\n                        <div id=\"divEditPermissionsLoader\" style=\"height: 500px;\"></div>\n\n                        <div id=\"divEditPermissionsViewer\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <div class=\"well well-sm\" style=\"background-color: #fbfbfb;\">\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-3 control-label\">User Permissions</label>\n                                    <div class=\"col-sm-9\">\n                                        <table id=\"tableEditPermissionsUser\" class=\"table table-hover\">\n                                            <thead>\n                                                <tr>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsUser', 0); return false;\">Username</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsUser', 0); return false;\" style=\"width: 65px;\">View</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsUser', 0); return false;\" style=\"width: 65px;\">Modify</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsUser', 0); return false;\" style=\"width: 65px;\">Delete</a></th>\n                                                    <th style=\"width: 76px;\"></th>\n                                                </tr>\n                                            </thead>\n                                            <tbody id=\"tbodyEditPermissionsUser\"></tbody>\n                                        </table>\n                                        <label class=\"control-label\" for=\"optEditPermissionsUserList\">Add User</label>\n                                        <select id=\"optEditPermissionsUserList\" class=\"form-control\"></select>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"well well-sm\" style=\"background-color: #fbfbfb;\">\n                                <div class=\"form-group\">\n                                    <label class=\"col-sm-3 control-label\">Group Permissions</label>\n                                    <div class=\"col-sm-9\">\n                                        <table id=\"tableEditPermissionsGroup\" class=\"table table-hover\">\n                                            <thead>\n                                                <tr>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsGroup', 0); return false;\">Group</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsGroup', 0); return false;\" style=\"width: 65px;\">View</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsGroup', 0); return false;\" style=\"width: 65px;\">Modify</a></th>\n                                                    <th><a href=\"#\" onclick=\"sortTable('tbodyEditPermissionsGroup', 0); return false;\" style=\"width: 65px;\">Delete</a></th>\n                                                    <th style=\"width: 76px;\"></th>\n                                                </tr>\n                                            </thead>\n                                            <tbody id=\"tbodyEditPermissionsGroup\"></tbody>\n                                        </table>\n                                        <label class=\"control-label\" for=\"optEditPermissionsGroupList\">Add Group</label>\n                                        <select id=\"optEditPermissionsGroupList\" class=\"form-control\"></select>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnEditPermissionsSave\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\">Save</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalInitializeNewCluster\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Initialize New Cluster</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divInitializeNewClusterAlert\"></div>\n\n                        <div id=\"divInitializeNewClusterLoader\" style=\"height: 350px;\"></div>\n\n                        <div id=\"divInitializeNewClusterView\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <p>\n                                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.\n                            </p>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtInitializeNewClusterDomain\" class=\"col-sm-4 control-label\">Cluster Domain</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtInitializeNewClusterDomain\" type=\"text\" class=\"form-control\" placeholder=\"domain name\" maxlength=\"255\">\n                                    <div style=\"padding-top: 5px;\">The fully qualified domain name to be used to identify the new Cluster.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtInitializeNewClusterPrimaryNodeIpAddresses\" class=\"col-sm-4 control-label\">Primary Node IP Addresses</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtInitializeNewClusterPrimaryNodeIpAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n\n                                    <label for=\"optInitializeNewClusterQuickIpAddresses\" class=\"control-label\">Quick Add</label>\n                                    <select id=\"optInitializeNewClusterQuickIpAddresses\" class=\"form-control\"></select>\n\n                                    <div style=\"padding-top: 5px;\">The static IP addresses of this DNS server that will be accessible by all other DNS Servers to be added later as Secondary nodes.</div>\n                                    <div style=\"padding-top: 5px;\">Enter IP addresses one below another in the above text field or use the Quick Add list to add available IP addresses on the server.</div>\n                                </div>\n                            </div>\n\n                            <p>\n                                <b>Note!</b> When the Cluster is initialized, the DNS Server Domain Name will be changed such that the current hostname is a subdomain name of the Cluster Domain name specified above. For example, if the current DNS Server Domain Name is <code>ns1.mydomain.tld</code> or just <code>ns1</code> then the new domain name will be <code>ns1.mycluster.tld</code>.\n                            </p>\n\n                            <p>\n                                <b>Note!</b> If the web service does not have HTTPS enabled, then the initialization process will enable it automatically with a self-signed certificate. However, it is recommended to manually configure HTTPS with a valid certificate before initializing the Cluster. This certificate must include the new expected DNS Server Domain Name, as mentioned in the above note, as the Subject Common Name or Subject Alternative Name (SAN) so that it validates when a Secondary node tries to join the Cluster. Once a node joins the Cluster, it uses DANE-EE for server authentication and the domain name in the certificate is no longer required to match the DNS Server Domain Name.\n                            </p>\n\n                            <p>\n                                <b>Note!</b> 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 where the Cluster will automatically manage domain name records for all the nodes. The second zone will be the Cluster Catalog zone that uses <code>cluster-catalog</code> 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.\n                            </p>\n\n                            <p>\n                                <b>Warning!</b> The Cluster Domain name cannot be changed later. Make sure that you enter the correct domain name before proceeding. You can update the DNS Server Domain Name later if needed from Settings but it must always be a subdomain name of the Cluster Domain name.\n                            </p>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <div class=\"pull-left\">\n                            <a href=\"https://blog.technitium.com/2025/11/understanding-clustering-and-how-to.html\" target=\"_blank\">Help: Understanding Clustering And How To Configure It</a>\n                        </div>\n                        <div class=\"pull-right\">\n                            <button type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Working...\" onclick=\"initializeNewCluster(this); return false;\">Initialize</button>\n                            <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalInitializeJoinCluster\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Join Cluster</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divInitializeJoinClusterAlert\"></div>\n\n                        <div id=\"divInitializeJoinClusterLoader\" style=\"height: 500px;\"></div>\n\n                        <div id=\"divInitializeJoinClusterView\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <p>\n                                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.\n                            </p>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtInitializeJoinClusterSecondaryNodeIpAddresses\" class=\"col-sm-4 control-label\">Secondary Node IP Addresses</label>\n                                <div class=\"col-sm-7\">\n                                    <textarea id=\"txtInitializeJoinClusterSecondaryNodeIpAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n\n                                    <label for=\"optInitializeJoinClusterQuickIpAddresses\" class=\"control-label\">Quick Add</label>\n                                    <select id=\"optInitializeJoinClusterQuickIpAddresses\" class=\"form-control\"></select>\n\n                                    <div style=\"padding-top: 5px;\">The static IP addresses of this DNS server that will be accessible by all other DNS Server nodes in the Cluster.</div>\n                                    <div style=\"padding-top: 5px;\">Enter IP addresses one below another in the above text field or use the Quick Add list to add available IP addresses on the server.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtInitializeJoinClusterPrimaryNodeUrl\" class=\"col-sm-4 control-label\">Primary Node URL</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtInitializeJoinClusterPrimaryNodeUrl\" type=\"text\" class=\"form-control\" placeholder=\"URL\" maxlength=\"255\">\n                                    <div style=\"padding-top: 5px;\">The web service HTTPS URL of the Primary node in the Cluster.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtInitializeJoinClusterPrimaryNodeIpAddress\" class=\"col-sm-4 control-label\">Primary Node IP Address (Optional)</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtInitializeJoinClusterPrimaryNodeIpAddress\" type=\"text\" class=\"form-control\" placeholder=\"IP address\" maxlength=\"255\">\n                                    <div style=\"padding-top: 5px;\">The IP address of the Primary node in the Cluster. When unspecified, domain name in the Primary node URL will be resolved and used.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label class=\"col-sm-4 control-label\">Certificate Validation</label>\n                                <div class=\"col-sm-7\">\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdInitializeJoinClusterCertificateValidation\" id=\"rdInitializeJoinClusterCertificateValidationDefault\" value=\"false\">\n                                            Validate Certificate With PKI and DANE (Recommended)\n                                        </label>\n                                        <div style=\"padding-top: 5px; padding-left: 20px;\">\n                                            The Primary node web service TLS certificate will be validated using PKI and DANE to ensure that your connection is secure.\n                                        </div>\n                                    </div>\n                                    <div class=\"radio\">\n                                        <label>\n                                            <input type=\"radio\" name=\"rdInitializeJoinClusterCertificateValidation\" id=\"rdInitializeJoinClusterCertificateValidationIgnore\" value=\"true\">\n                                            Ignore Certificate Validation Errors\n                                        </label>\n                                        <div style=\"padding-top: 5px; padding-left: 20px;\">\n                                            Use this options only when you know that the Primary node web service is using a self-signed TLS certificate and is reachable on a private network.\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtInitializeJoinClusterPrimaryNodeUsername\" class=\"col-sm-4 control-label\">Primary Node Username</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtInitializeJoinClusterPrimaryNodeUsername\" type=\"text\" class=\"form-control\" placeholder=\"username\" maxlength=\"255\">\n                                    <div style=\"padding-top: 5px;\">The username of an administrator on the Primary node in the Cluster.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtInitializeJoinClusterPrimaryNodePassword\" class=\"col-sm-4 control-label\">Primary Node Password</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtInitializeJoinClusterPrimaryNodePassword\" type=\"password\" class=\"form-control\" maxlength=\"255\">\n                                    <div style=\"padding-top: 5px;\">The password of the administrator user specified above.</div>\n                                </div>\n                            </div>\n\n                            <div id=\"divInitializeJoinClusterPrimaryNode2faTotp\" class=\"form-group\">\n                                <label for=\"txtInitializeJoinClusterPrimaryNode2faTotp\" class=\"col-sm-4 control-label\">Primary Node OTP</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtInitializeJoinClusterPrimaryNode2faTotp\" type=\"text\" class=\"form-control\" placeholder=\"OTP\" maxlength=\"6\">\n                                    <div style=\"padding-top: 5px;\">Enter the 6-digit code you see in your authenticator app for the administrator user specified above.</div>\n                                </div>\n                            </div>\n\n                            <p>\n                                <b>Note!</b> 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.\n                            </p>\n\n                            <p>\n                                <b>Note!</b> 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. This certificate should optionally include the new expected DNS Server Domain Name, which will be a subdomain name of the Cluster Domain name, as the Subject Common Name or Subject Alternative Name (SAN).\n                            </p>\n\n                            <p>\n                                <b>Note!</b> The Ignore Certificate Validation Errors option when selected is used only for the initial connection to the Primary node during the joining process. Once the DNS server joins the Cluster, it will always use DANE-EE for authentication using the TLSA records in the Cluster Primary zone that are added for each node in the Cluster.\n                            </p>\n\n                            <p>\n                                <b>Warning!</b> Joining a Cluster will cause configuration on this DNS Server to be overwritten permanently for Allowed, Blocked, Apps, Settings and Administration sections!\n                            </p>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <div class=\"pull-left\">\n                            <a href=\"https://blog.technitium.com/2025/11/understanding-clustering-and-how-to.html\" target=\"_blank\">Help: Understanding Clustering And How To Configure It</a>\n                        </div>\n                        <div class=\"pull-right\">\n                            <button type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Joining...\" onclick=\"initializeJoinCluster(this); return false;\">Join</button>\n                            <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalClusterOptions\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Cluster Options</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divClusterOptionsAlert\"></div>\n\n                        <div id=\"divClusterOptionsLoader\" style=\"height: 100px;\"></div>\n\n                        <div id=\"divClusterOptionsView\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <div class=\"form-group\">\n                                <label for=\"txtClusterOptionsClusterDomain\" class=\"col-sm-4 control-label\">Cluster Domain</label>\n                                <div class=\"col-sm-7\">\n                                    <input id=\"txtClusterOptionsClusterDomain\" type=\"text\" class=\"form-control\" placeholder=\"domain name\" maxlength=\"255\" disabled>\n                                    <div style=\"padding-top: 5px;\">The fully qualified domain name of the Cluster.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtClusterOptionsHeartbeatRefreshIntervalSeconds\" class=\"col-sm-4 control-label\">Heartbeat Refresh Interval</label>\n                                <div class=\"col-sm-7\">\n                                    <div>\n                                        <input id=\"txtClusterOptionsHeartbeatRefreshIntervalSeconds\" type=\"number\" class=\"form-control\" placeholder=\"seconds\" maxlength=\"5\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds (valid range 10-300; default 30)</span>\n                                    </div>\n                                    <div style=\"padding-top: 5px;\">The interval in seconds in which the DNS server must refresh the state of all nodes in the Cluster.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtClusterOptionsHeartbeatRetryIntervalSeconds\" class=\"col-sm-4 control-label\">Heartbeat Retry Interval</label>\n                                <div class=\"col-sm-7\">\n                                    <div>\n                                        <input id=\"txtClusterOptionsHeartbeatRetryIntervalSeconds\" type=\"number\" class=\"form-control\" placeholder=\"seconds\" maxlength=\"5\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds (valid range 10-300; default 10)</span>\n                                    </div>\n                                    <div style=\"padding-top: 5px;\">The interval in seconds in which the DNS server must retry the state refresh process for all nodes in case of a failure.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtClusterOptionsConfigRefreshIntervalSeconds\" class=\"col-sm-4 control-label\">Config Refresh Interval</label>\n                                <div class=\"col-sm-7\">\n                                    <div>\n                                        <input id=\"txtClusterOptionsConfigRefreshIntervalSeconds\" type=\"number\" class=\"form-control\" placeholder=\"seconds\" maxlength=\"5\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds (valid range 30-3600; default 900)</span>\n                                    </div>\n                                    <div style=\"padding-top: 5px;\">The interval in seconds in which the DNS server must refresh the configuration from the Primary node.</div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group\">\n                                <label for=\"txtClusterOptionsConfigRetryIntervalSeconds\" class=\"col-sm-4 control-label\">Config Retry Interval</label>\n                                <div class=\"col-sm-7\">\n                                    <div>\n                                        <input id=\"txtClusterOptionsConfigRetryIntervalSeconds\" type=\"number\" class=\"form-control\" placeholder=\"seconds\" maxlength=\"5\" style=\"width: 100px; display: inline;\">\n                                        <span>seconds (valid range 30-3600; default 60)</span>\n                                    </div>\n                                    <div style=\"padding-top: 5px;\">The interval in seconds in which the DNS server must retry the configuration refresh process for the Primary node in case of a failure.</div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnClusterOptionsSave\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\" onclick=\"saveClusterOptions(this); return false;\">Save</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalEditClusterNode\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\" style=\"width: 780px;\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Edit Node - <span id=\"lblEditClusterNodeName\">node1.example.com</span></h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divEditClusterNodeAlert\"></div>\n\n                        <div id=\"divEditClusterNodeLoader\" style=\"height: 100px;\"></div>\n\n                        <div id=\"divEditClusterNodeView\" style=\"max-height: 500px; overflow-y: auto; padding: 0 6px; overflow-x: hidden;\">\n                            <div id=\"divEditClusterNodeSelfNode\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditClusterNodeSelfNodeIpAddresses\" class=\"col-sm-4 control-label\">Node IP Addresses</label>\n                                    <div class=\"col-sm-7\">\n                                        <textarea id=\"txtEditClusterNodeSelfNodeIpAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n\n                                        <label for=\"optEditClusterNodeQuickSelfIpAddresses\" class=\"control-label\">Quick Add</label>\n                                        <select id=\"optEditClusterNodeQuickSelfIpAddresses\" class=\"form-control\"></select>\n\n                                        <div style=\"padding-top: 5px;\">The static IP addresses of this DNS server that will be accessible by all other DNS Server nodes in the Cluster.</div>\n                                        <div style=\"padding-top: 5px;\">Enter IP addresses one below another in the above text field or use the Quick Add list to add available IP addresses on the server.</div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div id=\"divEditClusterNodePrimaryNode\">\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditClusterNodePrimaryNodeUrl\" class=\"col-sm-4 control-label\">Primary Node URL</label>\n                                    <div class=\"col-sm-7\">\n                                        <input id=\"txtEditClusterNodePrimaryNodeUrl\" type=\"text\" class=\"form-control\" placeholder=\"URL\" maxlength=\"255\">\n                                        <div style=\"padding-top: 5px;\">The web service HTTPS URL of the Primary node in the Cluster.</div>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"txtEditClusterNodePrimaryNodeIpAddresses\" class=\"col-sm-4 control-label\">Primary Node IP Addresses (Optional)</label>\n                                    <div class=\"col-sm-7\">\n                                        <textarea id=\"txtEditClusterNodePrimaryNodeIpAddresses\" class=\"form-control\" rows=\"3\" spellcheck=\"false\"></textarea>\n\n                                        <div style=\"padding-top: 5px;\">The IP addresses of the Primary node in the Cluster. When unspecified, domain name in the Primary node URL will be resolved and used.</div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnEditClusterNodeSave\" type=\"submit\" class=\"btn btn-primary\" data-loading-text=\"Saving...\">Save</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalRemoveClusterNode\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Remove Node - <span id=\"lblRemoveClusterNodeName\">node1.example.com</span></h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divRemoveClusterNodeAlert\"></div>\n\n                        <p>\n                            The Remove Node process will ask the selected Secondary node to leave the Cluster gracefully. The Secondary will then initiate Leave Cluster process as if the Leave Cluster action was performed on that node itself.\n                        </p>\n\n                        <div class=\"form-group\">\n                            <div class=\"col-sm-offset-1 col-sm-10\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkRemoveClusterNodeForceRemove\" type=\"checkbox\"> Force Remove Node\n                                    </label>\n                                </div>\n                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enabling this option will cause the Secondary node to be deleted from the Cluster without asking the node to leave gracefully.</div>\n                            </div>\n                        </div>\n\n                        <p>\n                            Are you sure you want to remove the Secondary node from the Cluster?\n                        </p>\n\n                        <p>\n                            <b>Note!</b> Use the Force Remove Node option only when the Secondary node is unreachable/decommissioned and cannot leave the Cluster gracefully.\n                        </p>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button id=\"btnRemoveClusterNode\" type=\"submit\" class=\"btn btn-warning\" data-loading-text=\"Removing...\" onclick=\"removeSecondaryClusterNode(this); return false;\">Remove</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalPromoteToPrimaryClusterNode\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Promote To Primary Node - <span id=\"lblPromoteToPrimaryClusterNodeName\"></span></h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divPromoteToPrimaryClusterNodeAlert\"></div>\n\n                        <p>\n                            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.\n                        </p>\n\n                        <div class=\"form-group\">\n                            <div class=\"col-sm-offset-1 col-sm-10\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkPromoteToPrimaryClusterNodeForceDeletePrimary\" type=\"checkbox\"> Force Delete Current Primary Node\n                                    </label>\n                                </div>\n                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enabling this option will cause the current Primary node to be deleted from the Cluster without resyncing complete configuration from it and without inform it.</div>\n                            </div>\n                        </div>\n\n                        <p>\n                            Are you sure you want to promote the selected Secondary node to become the Primary node in the Cluster?\n                        </p>\n\n                        <p>\n                            <b>Note!</b> 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.\n                        </p>\n\n                        <p>\n                            <b>Note!</b> 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.\n                        </p>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button type=\"submit\" class=\"btn btn-warning\" data-loading-text=\"Promoting...\" onclick=\"promoteToPrimaryClusterNode(this); return false;\">Promote</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalLeaveCluster\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Leave Cluster</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divLeaveClusterAlert\"></div>\n\n                        <p>\n                            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.\n                        </p>\n\n                        <div class=\"form-group\">\n                            <div class=\"col-sm-offset-1 col-sm-10\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkLeaveClusterForceLeave\" type=\"checkbox\"> Force Leave Cluster\n                                    </label>\n                                </div>\n                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enabling this option will cause this Secondary node to leave the Cluster without informing the Primary node.</div>\n                            </div>\n                        </div>\n\n                        <p>\n                            Are you sure you want to leave the Cluster?\n                        </p>\n\n                        <p>\n                            <b>Note!</b> Use the Force Leave Cluster option only when the Primary node is unreachable/decommissioned and thus cannot leave the Cluster gracefully.\n                        </p>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button type=\"submit\" class=\"btn btn-warning\" data-loading-text=\"Leaving...\" onclick=\"leaveCluster(this); return false;\">Leave</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"modalDeleteCluster\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n        <form class=\"form-horizontal\">\n            <div class=\"modal-dialog\" role=\"document\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-header\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                        <h4 class=\"modal-title\">Delete Cluster</h4>\n                    </div>\n                    <div class=\"modal-body\">\n                        <div id=\"divDeleteClusterAlert\"></div>\n\n                        <p>\n                            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.\n                        </p>\n\n                        <div class=\"form-group\">\n                            <div class=\"col-sm-offset-1 col-sm-10\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input id=\"chkDeleteClusterForceDelete\" type=\"checkbox\"> Force Delete Cluster\n                                    </label>\n                                </div>\n                                <div style=\"padding-top: 5px; padding-left: 20px;\">Enabling this option will cause this Primary node to delete the Cluster for itself even when other Secondary nodes still exist, orphaning them.</div>\n                            </div>\n                        </div>\n\n                        <p>\n                            Are you sure you want to delete the Cluster?\n                        </p>\n\n                        <p>\n                            <b>Note!</b> 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.\n                        </p>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button type=\"submit\" class=\"btn btn-danger\" data-loading-text=\"Deleting...\" onclick=\"deleteCluster(this); return false;\">Delete</button>\n                        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n                    </div>\n                </div>\n            </div>\n        </form>\n    </div>\n\n    <div id=\"footer\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "DnsServerCore/www/js/apps.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nfunction refreshApps() {\n    var divViewAppsLoader = $(\"#divViewAppsLoader\");\n    var divViewApps = $(\"#divViewApps\");\n\n    divViewApps.hide();\n    divViewAppsLoader.show();\n\n    HTTPRequest({\n        url: \"api/apps/list?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var apps = responseJSON.response.apps;\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < apps.length; i++) {\n                tableHtmlRows += getAppRowHtml(apps[i]);\n            }\n\n            $(\"#tableAppsBody\").html(tableHtmlRows);\n\n            if (apps.length > 0)\n                $(\"#tableAppsFooter\").html(\"<tr><td colspan=\\\"3\\\"><b>Total Apps: \" + apps.length + \"</b></td></tr>\");\n            else\n                $(\"#tableAppsFooter\").html(\"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Apps Found</td></tr>\");\n\n            divViewAppsLoader.hide();\n            divViewApps.show();\n        },\n        error: function () {\n            divViewAppsLoader.hide();\n            divViewApps.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divViewAppsLoader\n    });\n}\n\nfunction getAppRowId(appName) {\n    return btoa(appName).replace(/=/g, \"\");\n}\n\nfunction getAppRowHtml(app) {\n    var name = app.name;\n    var version = app.version;\n    var updateVersion = app.updateVersion;\n    var updateUrl = app.updateUrl;\n    var updateAvailable = app.updateAvailable;\n\n    var dnsAppsTable = null;\n\n    //dnsApps\n    if (app.dnsApps.length > 0) {\n        dnsAppsTable = \"<table class=\\\"table\\\" style=\\\"margin-bottom: 10px; background: transparent;\\\"><thead><th>Class Path</th><th>Description</th></thead><tbody>\";\n\n        for (var j = 0; j < app.dnsApps.length; j++) {\n            var labels = \"\";\n            var description = null;\n\n            if (app.dnsApps[j].isAppRecordRequestHandler) {\n                labels += \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">APP Record</span>\";\n                description = \"<p>\" + htmlEncode(app.dnsApps[j].description).replace(/\\n/g, \"<br />\") + \"</p>\" + (app.dnsApps[j].recordDataTemplate == null ? \"\" : \"<div><b>Record Data Template</b><pre>\" + htmlEncode(app.dnsApps[j].recordDataTemplate) + \"</pre></div>\");\n            }\n\n            if (app.dnsApps[j].isRequestController)\n                labels += \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">Access Control</span>\";\n\n            if (app.dnsApps[j].isAuthoritativeRequestHandler)\n                labels += \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">Authoritative</span>\";\n\n            if (app.dnsApps[j].isRequestBlockingHandler)\n                labels += \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">Blocking</span>\";\n\n            if (app.dnsApps[j].isQueryLogger)\n                labels += \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">Query Logger</span>\";\n\n            if (app.dnsApps[j].isQueryLogs)\n                labels += \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">Query Logs</span>\";\n\n            if (app.dnsApps[j].isPostProcessor)\n                labels += \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">Post Processor</span>\";\n\n            if (labels == \"\")\n                labels = \"<span class=\\\"label label-info\\\" style=\\\"margin-right: 4px;\\\">Generic</span>\";\n\n            if (description == null)\n                description = htmlEncode(app.dnsApps[j].description).replace(/\\n/g, \"<br />\");\n\n            dnsAppsTable += \"<tr><td>\" + htmlEncode(app.dnsApps[j].classPath) + \"</br>\" + labels + \"</td><td>\" + description + \"</td></tr>\";\n        }\n\n        dnsAppsTable += \"</tbody></table>\"\n    }\n\n    var id = getAppRowId(name);\n    var tableHtmlRow = \"<tr id=\\\"trApp\" + id + \"\\\"><td><div><span style=\\\"font-weight: bold; font-size: 16px;\\\">\" + htmlEncode(name) + \"</span><br /><span id=\\\"trAppVersion\" + id + \"\\\" class=\\\"label label-primary\\\">Version \" + htmlEncode(version) + \"</span> <span id=\\\"trAppUpdateVersion\" + id + \"\\\" class=\\\"label label-warning\\\" style=\\\"\" + (updateAvailable ? \"\" : \"display: none;\") + \"\\\">Update \" + htmlEncode(updateVersion) + \"</span></div>\";\n\n    if (app.description != null)\n        tableHtmlRow += \"<div style=\\\"margin-top: 10px;\\\">\" + htmlEncode(app.description).replace(/\\n/g, \"<br />\") + \"</div>\";\n\n    if (dnsAppsTable != null) {\n        tableHtmlRow += \"<div style=\\\"margin-top: 10px;\\\"><a href=\\\"#\" + id + \"\\\" class=\\\"collapsed\\\" data-toggle=\\\"collapse\\\" aria-expanded=\\\"false\\\" aria-controls=\\\"\" + id + \"\\\">More Details <span class=\\\"glyphicon glyphicon-chevron-down\\\" style=\\\"font-size: 10px;\\\" aria-hidden=\\\"true\\\"></span></a>\";\n        tableHtmlRow += \"<div id=\\\"\" + id + \"\\\" class=\\\"collapse\\\" aria-expanded=\\\"false\\\">\";\n        tableHtmlRow += dnsAppsTable;\n        tableHtmlRow += \"</div></div>\";\n    }\n\n    tableHtmlRow += \"</td>\";\n    tableHtmlRow += \"<td><button type=\\\"button\\\" class=\\\"btn btn-default\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 80px; margin-bottom: 6px; display: block;\\\" onclick=\\\"showAppConfigModal(this, '\" + name + \"');\\\" data-loading-text=\\\"Loading...\\\">Config</button>\";\n    tableHtmlRow += \"<button type=\\\"button\\\" class=\\\"btn btn-warning\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 80px; margin-bottom: 6px; display: block;\\\" onclick=\\\"showUpdateAppModal('\" + name + \"');\\\">Update</button>\";\n    tableHtmlRow += \"<button id=\\\"btnAppsStoreUpdate\" + id + \"\\\" type=\\\"button\\\" data-id=\\\"\" + id + \"\\\" class=\\\"btn btn-warning\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 80px; margin-bottom: 6px; \" + (updateAvailable ? \"\" : \"display: none;\") + \"\\\" onclick=\\\"updateStoreApp(this, '\" + name + \"', '\" + updateUrl + \"', false);\\\" data-loading-text=\\\"Updating...\\\">Store Update</button>\";\n    tableHtmlRow += \"<button type=\\\"button\\\" data-id=\\\"\" + id + \"\\\" class=\\\"btn btn-danger\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 80px; margin-bottom: 6px; display: block;\\\" onclick=\\\"uninstallApp(this, '\" + name + \"');\\\" data-loading-text=\\\"Uninstalling...\\\">Uninstall</button></td></tr>\";\n\n    return tableHtmlRow\n}\n\nfunction showStoreAppsModal() {\n    var divStoreAppsAlert = $(\"#divStoreAppsAlert\");\n    var divStoreAppsLoader = $(\"#divStoreAppsLoader\");\n    var divStoreApps = $(\"#divStoreApps\");\n\n    divStoreAppsLoader.show();\n    divStoreApps.hide();\n    $(\"#modalStoreApps\").modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/apps/listStoreApps?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var storeApps = responseJSON.response.storeApps;\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < storeApps.length; i++) {\n                var id = Math.floor(Math.random() * 10000);\n                var name = storeApps[i].name;\n                var version = storeApps[i].version;\n                var description = storeApps[i].description;\n                var url = storeApps[i].url;\n                var size = storeApps[i].size;\n                var installed = storeApps[i].installed;\n                var installedVersion = storeApps[i].installedVersion;\n                var updateAvailable = installed ? storeApps[i].updateAvailable : false;\n\n                var displayVersion = installed ? installedVersion : version;\n                description = htmlEncode(description).replace(/\\n/g, \"<br />\");\n\n                tableHtmlRows += \"<tr id=\\\"trStoreApp\" + id + \"\\\"><td><div style=\\\"margin-bottom: 14px;\\\"><span style=\\\"font-weight: bold; font-size: 16px;\\\">\" + htmlEncode(name) + \"</span><br /><span id=\\\"spanStoreAppDisplayVersion\" + id + \"\\\" class=\\\"label label-primary\\\">Version \" + htmlEncode(displayVersion) + \"</span> <span id=\\\"spanStoreAppUpdateVersion\" + id + \"\\\" class=\\\"label label-warning\\\" style=\\\"\" + (updateAvailable ? \"\" : \"display: none;\") + \"\\\">Update \" + htmlEncode(version) + \"</span></div>\";\n                tableHtmlRows += \"<div style=\\\"margin-bottom: 10px;\\\">\" + description + \"</div><div><b>App Zip File</b>: \" + htmlEncode(url) + \"<br /><b>Size</b>: \" + htmlEncode(size) + \"</div></td><td>\";\n                tableHtmlRows += \"<button id=\\\"btnStoreAppInstall\" + id + \"\\\" type=\\\"button\\\" data-id=\\\"\" + id + \"\\\" class=\\\"btn btn-primary\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 80px; margin-bottom: 6px; \" + (installed ? \"display: none;\" : \"\") + \"\\\" onclick=\\\"installStoreApp(this, '\" + name + \"', '\" + url + \"');\\\" data-loading-text=\\\"Installing...\\\">Install</button>\";\n                tableHtmlRows += \"<button id=\\\"btnStoreAppUpdate\" + id + \"\\\" type=\\\"button\\\" data-id=\\\"\" + id + \"\\\" class=\\\"btn btn-warning\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 80px; margin-bottom: 6px; \" + (updateAvailable ? \"\" : \"display: none;\") + \"\\\" onclick=\\\"updateStoreApp(this, '\" + name + \"', '\" + url + \"', true);\\\" data-loading-text=\\\"Updating...\\\">Update</button>\";\n                tableHtmlRows += \"<button id=\\\"btnStoreAppUninstall\" + id + \"\\\" type=\\\"button\\\" data-id=\\\"\" + id + \"\\\" class=\\\"btn btn-danger\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 80px; margin-bottom: 6px; \" + (installed ? \"\" : \"display: none;\") + \"\\\" onclick=\\\"uninstallStoreApp(this, '\" + name + \"');\\\" data-loading-text=\\\"Uninstalling...\\\">Uninstall</button>\";\n                tableHtmlRows += \"</td></tr>\";\n            }\n\n            $(\"#tableStoreAppsBody\").html(tableHtmlRows);\n\n            if (storeApps.length > 0)\n                $(\"#tableStoreAppsFooter\").html(\"<tr><td colspan=\\\"3\\\"><b>Total Apps: \" + storeApps.length + \"</b></td></tr>\");\n            else\n                $(\"#tableStoreAppsFooter\").html(\"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Apps Found</td></tr>\");\n\n            divStoreAppsLoader.hide();\n            divStoreApps.show();\n        },\n        error: function () {\n            divStoreAppsLoader.hide();\n            divStoreApps.show();\n        },\n        invalidToken: function () {\n            $(\"#modalStoreApps\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divStoreAppsAlert,\n        objLoaderPlaceholder: divStoreAppsLoader\n    });\n}\n\nfunction showInstallAppModal() {\n    $(\"#divInstallAppAlert\").html(\"\");\n    $(\"#txtInstallApp\").val(\"\");\n    $(\"#fileAppZip\").val(\"\");\n    $(\"#btnInstallApp\").button(\"reset\");\n\n    $(\"#modalInstallApp\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtInstallApp\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction showUpdateAppModal(appName) {\n    $(\"#divUpdateAppAlert\").html(\"\");\n    $(\"#txtUpdateApp\").val(appName);\n    $(\"#fileUpdateAppZip\").val(\"\");\n    $(\"#btnUpdateApp\").button(\"reset\");\n\n    $(\"#modalUpdateApp\").modal(\"show\");\n}\n\nfunction installStoreApp(objBtn, appName, url) {\n    var divStoreAppsAlert = $(\"#divStoreAppsAlert\");\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/downloadAndInstall?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName) + \"&url=\" + encodeURIComponent(url),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            btn.hide();\n\n            var id = btn.attr(\"data-id\");\n            $(\"#btnStoreAppUninstall\" + id).show();\n\n            var tableHtmlRow = getAppRowHtml(responseJSON.response.installedApp);\n            $(\"#tableAppsBody\").prepend(tableHtmlRow);\n            updateAppsFooterCount();\n\n            showAlert(\"success\", \"Store App Installed!\", \"DNS application '\" + appName + \"' was installed successfully from DNS App Store.\", divStoreAppsAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalStoreApps\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divStoreAppsAlert\n    });\n}\n\nfunction updateStoreApp(objBtn, appName, url, isModal) {\n    var divStoreAppsAlert;\n\n    if (isModal)\n        divStoreAppsAlert = $(\"#divStoreAppsAlert\");\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/downloadAndUpdate?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName) + \"&url=\" + encodeURIComponent(url),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            btn.hide();\n\n            if (isModal) {\n                var id = btn.attr(\"data-id\");\n                $(\"#spanStoreAppUpdateVersion\" + id).hide();\n                $(\"#spanStoreAppDisplayVersion\" + id).text($(\"#spanStoreAppUpdateVersion\" + id).text().replace(/Update/g, \"Version\"));\n            }\n\n            var tableHtmlRow = getAppRowHtml(responseJSON.response.updatedApp);\n            var id = getAppRowId(responseJSON.response.updatedApp.name);\n            $(\"#trApp\" + id).replaceWith(tableHtmlRow);\n\n            showAlert(\"success\", \"Store App Updated!\", \"DNS application '\" + appName + \"' was updated successfully from DNS App Store.\", divStoreAppsAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalStoreApps\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divStoreAppsAlert\n    });\n}\n\nfunction uninstallStoreApp(objBtn, appName) {\n    if (!confirm(\"Are you sure you want to uninstall the DNS application '\" + appName + \"'?\"))\n        return;\n\n    var divStoreAppsAlert = $(\"#divStoreAppsAlert\");\n    var btn = $(objBtn);\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/uninstall?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            btn.hide();\n\n            var id = btn.attr(\"data-id\");\n            $(\"#btnStoreAppInstall\" + id).show();\n            $(\"#btnStoreAppUpdate\" + id).hide();\n            $(\"#spanStoreAppVersion\" + id).attr(\"class\", \"label label-primary\");\n\n            var id = getAppRowId(appName);\n            $(\"#trApp\" + id).remove();\n            updateAppsFooterCount();\n\n            showAlert(\"success\", \"Store App Uninstalled!\", \"DNS application '\" + appName + \"' was uninstalled successfully.\", divStoreAppsAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalStoreApps\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divStoreAppsAlert\n    });\n}\n\nfunction installApp() {\n    var divInstallAppAlert = $(\"#divInstallAppAlert\");\n    var appName = $(\"#txtInstallApp\").val();\n\n    if ((appName === null) || (appName === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter an application name.\", divInstallAppAlert);\n        $(\"#txtInstallApp\").trigger(\"focus\");\n        return;\n    }\n\n    var fileAppZip = $(\"#fileAppZip\");\n\n    if (fileAppZip[0].files.length === 0) {\n        showAlert(\"warning\", \"Missing!\", \"Please select an application zip file to install.\", divInstallAppAlert);\n        fileAppZip.trigger(\"focus\");\n        return;\n    }\n\n    var formData = new FormData();\n    formData.append(\"fileAppZip\", $(\"#fileAppZip\")[0].files[0]);\n\n    var btn = $(\"#btnInstallApp\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/install?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName),\n        method: \"POST\",\n        data: formData,\n        contentType: false,\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalInstallApp\").modal(\"hide\");\n\n            var tableHtmlRow = getAppRowHtml(responseJSON.response.installedApp);\n            $(\"#tableAppsBody\").prepend(tableHtmlRow);\n            updateAppsFooterCount();\n\n            showAlert(\"success\", \"App Installed!\", \"DNS application '\" + appName + \"' was installed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalInstallApp\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divInstallAppAlert\n    });\n}\n\nfunction updateApp() {\n    var divUpdateAppAlert = $(\"#divUpdateAppAlert\");\n    var appName = $(\"#txtUpdateApp\").val();\n    var fileAppZip = $(\"#fileUpdateAppZip\");\n\n    if (fileAppZip[0].files.length === 0) {\n        showAlert(\"warning\", \"Missing!\", \"Please select an application zip file to update.\", divUpdateAppAlert);\n        fileAppZip.trigger(\"focus\");\n        return;\n    }\n\n    var formData = new FormData();\n    formData.append(\"fileAppZip\", $(\"#fileUpdateAppZip\")[0].files[0]);\n\n    var btn = $(\"#btnUpdateApp\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/update?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName),\n        method: \"POST\",\n        data: formData,\n        contentType: false,\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalUpdateApp\").modal(\"hide\");\n\n            var tableHtmlRow = getAppRowHtml(responseJSON.response.updatedApp);\n            var id = getAppRowId(responseJSON.response.updatedApp.name);\n            $(\"#trApp\" + id).replaceWith(tableHtmlRow);\n\n            showAlert(\"success\", \"App Updated!\", \"DNS application '\" + appName + \"' was updated successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalUpdateApp\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divUpdateAppAlert\n    });\n}\n\nfunction uninstallApp(objBtn, appName) {\n    if (!confirm(\"Are you sure you want to uninstall the DNS application '\" + appName + \"'?\"))\n        return;\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/uninstall?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName),\n        success: function (responseJSON) {\n            var id = btn.attr(\"data-id\");\n            $(\"#trApp\" + id).remove();\n            updateAppsFooterCount();\n\n            showAlert(\"success\", \"App Uninstalled!\", \"DNS application '\" + appName + \"' was uninstalled successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction updateAppsFooterCount() {\n    var totalApps = $(\"#tableApps >tbody >tr\").length;\n    if (totalApps > 0)\n        $(\"#tableAppsFooter\").html(\"<tr><td colspan=\\\"3\\\"><b>Total Apps: \" + totalApps + \"</b></td></tr>\");\n    else\n        $(\"#tableAppsFooter\").html(\"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No App Found</td></tr>\");\n}\n\nfunction showAppConfigModal(objBtn, appName) {\n    var node = getPrimaryClusterNodeName(); //always reading app config from primary node to avoid issues due to config propagation delays\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/config/get?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n\n            $(\"#divAppConfigAlert\").html(\"\");\n\n            $(\"#lblAppConfigName\").html(appName);\n            $(\"#txtAppConfig\").val(responseJSON.response.config);\n\n            $(\"#btnAppConfig\").button(\"reset\");\n\n            $(\"#modalAppConfig\").modal(\"show\");\n\n            setTimeout(function () {\n                $(\"#txtAppConfig\").trigger(\"focus\");\n            }, 1000);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction saveAppConfig() {\n    var divAppConfigAlert = $(\"#divAppConfigAlert\");\n\n    var appName = $(\"#lblAppConfigName\").text();\n    var config = $(\"#txtAppConfig\").val();\n\n    var btn = $(\"#btnAppConfig\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/apps/config/set?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(appName),\n        method: \"POST\",\n        data: \"config=\" + encodeURIComponent(config),\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalAppConfig\").modal(\"hide\");\n\n            showAlert(\"success\", \"App Config Saved!\", \"The DNS application '\" + appName + \"' config was saved and reloaded successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalAppConfig\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAppConfigAlert\n    });\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/auth.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nvar sessionData = null;\n\n$(function () {\n    var token = localStorage.getItem(\"token\");\n    if (token == null) {\n        showPageLogin();\n        login(\"admin\", \"admin\");\n    }\n    else {\n        HTTPRequest({\n            url: \"api/user/session/get?token=\" + token,\n            success: function (responseJSON) {\n                sessionData = responseJSON;\n                localStorage.setItem(\"token\", sessionData.token);\n\n                $(\"#mnuUserDisplayName\").text(sessionData.displayName);\n                document.title = sessionData.info.dnsServerDomain + \" - \" + \"Technitium DNS Server v\" + sessionData.info.version;\n                $(\"#lblAboutVersion\").text(sessionData.info.version);\n                $(\"#lblAboutUptime\").text(moment(sessionData.info.uptimestamp).local().format(\"lll\") + \" (\" + moment(sessionData.info.uptimestamp).fromNow() + \")\");\n                $(\"#lblDnsServerDomain\").text(\" - \" + sessionData.info.dnsServerDomain);\n                $(\"#chkUseSoaSerialDateScheme\").prop(\"checked\", sessionData.info.useSoaSerialDateScheme);\n                $(\"#chkDnssecValidation\").prop(\"checked\", sessionData.info.dnssecValidation);\n\n                showPageMain();\n            },\n            error: function () {\n                showPageLogin();\n            }\n        });\n    }\n\n    $(\"#optGroupDetailsUserList\").on(\"change\", function () {\n        var selectedUser = $(\"#optGroupDetailsUserList\").val();\n\n        switch (selectedUser) {\n            case \"blank\":\n                break;\n\n            case \"none\":\n                $(\"#txtGroupDetailsMembers\").val(\"\");\n                break;\n\n            default:\n                var existingUsers = $(\"#txtGroupDetailsMembers\").val();\n                var existingUsersArray = existingUsers.split(\"\\n\");\n                var found = false;\n\n                for (var i = 0; i < existingUsersArray.length; i++) {\n                    if (existingUsersArray[i] === selectedUser) {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found) {\n                    if ((existingUsers.length > 0) && !existingUsers.endsWith(\"\\n\"))\n                        existingUsers += \"\\n\";\n\n                    existingUsers += selectedUser + \"\\n\";\n                    $(\"#txtGroupDetailsMembers\").val(existingUsers);\n                }\n                break;\n        }\n    });\n\n    $(\"#optUserDetailsGroupList\").on(\"change\", function () {\n        var selectedGroup = $(\"#optUserDetailsGroupList\").val();\n\n        switch (selectedGroup) {\n            case \"blank\":\n                break;\n\n            case \"none\":\n                $(\"#txtUserDetailsMemberOf\").val(\"\");\n                break;\n\n            default:\n                var existingGroups = $(\"#txtUserDetailsMemberOf\").val();\n                var existingGroupsArray = existingGroups.split(\"\\n\");\n                var found = false;\n\n                for (var i = 0; i < existingGroupsArray.length; i++) {\n                    if (existingGroupsArray[i] === selectedGroup) {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found) {\n                    if ((existingGroups.length > 0) && !existingGroups.endsWith(\"\\n\"))\n                        existingGroups += \"\\n\";\n\n                    existingGroups += selectedGroup + \"\\n\";\n                    $(\"#txtUserDetailsMemberOf\").val(existingGroups);\n                }\n                break;\n        }\n    });\n\n    $(\"#optEditPermissionsUserList\").on(\"change\", function () {\n        var selectedUser = $(\"#optEditPermissionsUserList\").val();\n\n        switch (selectedUser) {\n            case \"blank\":\n                break;\n\n            case \"none\":\n                $(\"#tbodyEditPermissionsUser\").html(\"\");\n                break;\n\n            default:\n                var data = serializeTableData($(\"#tableEditPermissionsUser\"), 4);\n                var parts = data.split(\"|\");\n                var found = false;\n\n                for (var i = 0; i < parts.length; i += 4) {\n                    if (parts[i] === selectedUser) {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found)\n                    addEditPermissionUserRow(null, selectedUser, false, false, false);\n\n                break;\n        }\n    });\n\n    $(\"#optEditPermissionsGroupList\").on(\"change\", function () {\n        var selectedGroup = $(\"#optEditPermissionsGroupList\").val();\n\n        switch (selectedGroup) {\n            case \"blank\":\n                break;\n\n            case \"none\":\n                $(\"#tbodyEditPermissionsGroup\").html(\"\");\n                break;\n\n            default:\n                var data = serializeTableData($(\"#tableEditPermissionsGroup\"), 4);\n                var parts = data.split(\"|\");\n                var found = false;\n\n                for (var i = 0; i < parts.length; i += 4) {\n                    if (parts[i] === selectedGroup) {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found)\n                    addEditPermissionGroupRow(null, selectedGroup, false, false, false);\n\n                break;\n        }\n    });\n});\n\nfunction login(username, password) {\n    var autoLogin = false;\n\n    if (username == null) {\n        username = $(\"#txtUser\").val().toLowerCase();\n        password = $(\"#txtPass\").val();\n    }\n    else {\n        autoLogin = true;\n    }\n\n    if ((username === null) || (username === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter an username.\");\n        $(\"#txtUser\").trigger(\"focus\");\n        return;\n    }\n\n    if ((password === null) || (password === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a password.\");\n        $(\"#txtPass\").trigger(\"focus\");\n        return;\n    }\n\n    var totp = $(\"#txt2FATOTP\").val();\n\n    if ($(\"#div2FAOTP\").is(\":visible\")) {\n        if ((totp == null) || (totp.length != 6)) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter the 6-digit OTP that you see in your authenticator app.\");\n            $(\"#txt2FATOTP\").trigger(\"focus\");\n            return;\n        }\n    }\n\n    var btn = $(\"#btnLogin\").button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/user/login\",\n        method: \"POST\",\n        data: \"user=\" + encodeURIComponent(username) + \"&pass=\" + encodeURIComponent(password) + \"&totp=\" + encodeURIComponent(totp) + \"&includeInfo=true\",\n        procecssData: false,\n        success: function (responseJSON) {\n            sessionData = responseJSON;\n            localStorage.setItem(\"token\", sessionData.token);\n\n            $(\"#mnuUserDisplayName\").text(sessionData.displayName);\n            document.title = sessionData.info.dnsServerDomain + \" - \" + \"Technitium DNS Server v\" + sessionData.info.version;\n            $(\"#lblAboutVersion\").text(sessionData.info.version);\n            $(\"#lblAboutUptime\").text(moment(sessionData.info.uptimestamp).local().format(\"lll\") + \" (\" + moment(sessionData.info.uptimestamp).fromNow() + \")\");\n            $(\"#lblDnsServerDomain\").text(\" - \" + sessionData.info.dnsServerDomain);\n\n            showPageMain();\n\n            if (!sessionData.totpEnabled && (username === \"admin\") && (password === \"admin\"))\n                showChangePasswordModal(password);\n        },\n        error: function () {\n            btn.button(\"reset\");\n\n            if ($(\"#div2FAOTP\").is(\":visible\")) {\n                $(\"#txt2FATOTP\").val(\"\");\n                $(\"#txt2FATOTP\").trigger(\"focus\");\n            }\n            else {\n                $(\"#txtUser\").trigger(\"focus\");\n            }\n\n            if (autoLogin)\n                hideAlert();\n        },\n        twoFactorAuthRequired: function () {\n            btn.button(\"reset\");\n\n            if (autoLogin) {\n                $(\"#txtUser\").trigger(\"focus\");\n            }\n            else {\n                $(\"#txtPass\").prop(\"disabled\", true);\n                $(\"#div2FAOTP\").show();\n                $(\"#txt2FATOTP\").trigger(\"focus\");\n            }\n        }\n    });\n}\n\nfunction logout() {\n    HTTPRequest({\n        url: \"api/user/logout?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            sessionData = null;\n            showPageLogin();\n        },\n        error: function () {\n            sessionData = null;\n            showPageLogin();\n        }\n    });\n}\n\nfunction showCreateMyApiTokenModal() {\n    $(\"#divCreateApiTokenAlert\").html(\"\");\n    $(\"#txtCreateApiTokenUsername\").val(sessionData.username);\n    $(\"#txtCreateApiTokenPassword\").val(\"\");\n    $(\"#txtCreateApiToken2FATOTP\").val(\"\");\n    $(\"#txtCreateApiTokenName\").val(\"\");\n\n    $(\"#txtCreateApiTokenUsername\").show();\n    $(\"#optCreateApiTokenUsername\").hide();\n    $(\"#divCreateApiTokenPassword\").show();\n\n    if (sessionData.totpEnabled)\n        $(\"#divCreateApiToken2FAOTP\").show();\n    else\n        $(\"#divCreateApiToken2FAOTP\").hide();\n\n    $(\"#divCreateApiTokenLoader\").hide();\n    $(\"#divCreateApiTokenForm\").show();\n    $(\"#divCreateApiTokenOutput\").hide();\n\n    var btnCreateApiToken = $(\"#btnCreateApiToken\");\n    btnCreateApiToken.attr(\"onclick\", \"createMyApiToken(this); return false;\");\n    btnCreateApiToken.show();\n\n    $(\"#modalCreateApiToken\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtCreateApiTokenPassword\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction createMyApiToken(objBtn) {\n    var btn = $(objBtn);\n\n    var divCreateApiTokenAlert = $(\"#divCreateApiTokenAlert\");\n\n    var user = $(\"#txtCreateApiTokenUsername\").val();\n    var password = $(\"#txtCreateApiTokenPassword\").val();\n    var totp = $(\"#txtCreateApiToken2FATOTP\").val();\n    var tokenName = $(\"#txtCreateApiTokenName\").val();\n\n    if (password === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a password.\", divCreateApiTokenAlert);\n        $(\"#txtCreateApiTokenPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if (sessionData.totpEnabled) {\n        if (totp.length != 6) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter the 6-digit OTP that you see in your authenticator app.\", divCreateApiTokenAlert);\n            $(\"#txtCreateApiToken2FATOTP\").trigger(\"focus\");\n            return;\n        }\n    }\n\n    if (tokenName === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a token name.\", divCreateApiTokenAlert);\n        $(\"#txtCreateApiTokenName\").trigger(\"focus\");\n        return;\n    }\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/user/createToken\",\n        method: \"POST\",\n        data: \"user=\" + encodeURIComponent(user) + \"&pass=\" + encodeURIComponent(password) + \"&totp=\" + encodeURIComponent(totp) + \"&tokenName=\" + encodeURIComponent(tokenName),\n        processData: false,\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            btn.hide();\n\n            $(\"#lblCreateApiTokenOutputUsername\").text(responseJSON.username);\n            $(\"#lblCreateApiTokenOutputTokenName\").text(responseJSON.tokenName);\n            $(\"#lblCreateApiTokenOutputToken\").text(responseJSON.token);\n\n            $(\"#divCreateApiTokenForm\").hide();\n            $(\"#divCreateApiTokenOutput\").show();\n\n            showAlert(\"success\", \"Token Created!\", \"API token was created successfully.\", divCreateApiTokenAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalCreateApiToken\").hide(\"\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divCreateApiTokenAlert\n    });\n}\n\nfunction showChangePasswordModal(currentPassword) {\n    $(\"#titleChangePassword\").text(\"Change Password\");\n\n    hideAlert($(\"#divChangePasswordAlert\"));\n    $(\"#txtChangePasswordUsername\").val(sessionData.username);\n\n    var txtChangePasswordCurrentPassword = $(\"#txtChangePasswordCurrentPassword\");\n\n    if (currentPassword == null) {\n        txtChangePasswordCurrentPassword.val(\"\");\n        txtChangePasswordCurrentPassword.prop(\"disabled\", false);\n    }\n    else {\n        txtChangePasswordCurrentPassword.val(currentPassword);\n        txtChangePasswordCurrentPassword.prop(\"disabled\", true);\n    }\n\n    $(\"#divChangePasswordCurrentPassword\").show();\n\n    $(\"#txtChangePasswordNewPassword\").val(\"\");\n    $(\"#txtChangePasswordConfirmPassword\").val(\"\");\n\n    $(\"#txtChangePassword2FATOTP\").val(\"\");\n\n    if (sessionData.totpEnabled)\n        $(\"#divChangePassword2FATOTP\").show();\n    else\n        $(\"#divChangePassword2FATOTP\").hide();\n\n    var btnChangePassword = $(\"#btnChangePassword\");\n    btnChangePassword.text(\"Change\");\n    btnChangePassword.attr(\"onclick\", \"changePassword(this); return false;\");\n    btnChangePassword.show();\n\n    $(\"#modalChangePassword\").modal(\"show\");\n\n    setTimeout(function () {\n        if (currentPassword == null)\n            $(\"#txtChangePasswordCurrentPassword\").trigger(\"focus\");\n        else\n            $(\"#txtChangePasswordNewPassword\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction changePassword(objBtn) {\n    var btn = $(objBtn);\n\n    var divChangePasswordAlert = $(\"#divChangePasswordAlert\");\n\n    var password = $(\"#txtChangePasswordCurrentPassword\").val();\n    var newPassword = $(\"#txtChangePasswordNewPassword\").val();\n    var confirmPassword = $(\"#txtChangePasswordConfirmPassword\").val();\n    var totp = $(\"#txtChangePassword2FATOTP\").val();\n\n    if ((password === null) || (password === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter the current password.\", divChangePasswordAlert);\n        $(\"#txtChangePasswordCurrentPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if ((newPassword === null) || (newPassword === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter new password.\", divChangePasswordAlert);\n        $(\"#txtChangePasswordNewPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if ((confirmPassword === null) || (confirmPassword === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter confirm password.\", divChangePasswordAlert);\n        $(\"#txtChangePasswordConfirmPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if (newPassword !== confirmPassword) {\n        showAlert(\"warning\", \"Mismatch!\", \"Passwords do not match. Please try again.\", divChangePasswordAlert);\n        $(\"#txtChangePasswordNewPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if (sessionData.totpEnabled) {\n        if ((totp == null) || (totp.length != 6)) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter the 6-digit OTP that you see in your authenticator app.\", divChangePasswordAlert);\n            $(\"#txtChangePassword2FATOTP\").trigger(\"focus\");\n            return;\n        }\n    }\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/user/changePassword\",\n        method: \"POST\",\n        data: \"token=\" + sessionData.token + \"&pass=\" + encodeURIComponent(password) + \"&newPass=\" + encodeURIComponent(newPassword) + \"&totp=\" + encodeURIComponent(totp),\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalChangePassword\").modal(\"hide\");\n            $(\"#txtChangePasswordCurrentPassword\").val(\"\");\n            $(\"#txtChangePasswordNewPassword\").val(\"\");\n            $(\"#txtChangePasswordConfirmPassword\").val(\"\");\n            $(\"#txtChangePassword2FATOTP\").val(\"\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Password Changed!\", \"Password was changed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalChangePassword\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divChangePasswordAlert\n    });\n}\n\nfunction showConfigure2FAModal() {\n    var divConfigure2FAAlert = $(\"#divConfigure2FAAlert\");\n    var divConfigure2FALoader = $(\"#divConfigure2FALoader\");\n    var divConfigure2FAViewer = $(\"#divConfigure2FAViewer\");\n    var btnEnable2FA = $(\"#btnEnable2FA\");\n    var btnDisable2FA = $(\"#btnDisable2FA\");\n\n    divConfigure2FALoader.show();\n    divConfigure2FAViewer.hide();\n\n    btnEnable2FA.hide();\n    btnDisable2FA.hide();\n\n    var modalConfigure2FA = $(\"#modalConfigure2FA\");\n    modalConfigure2FA.modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/user/2fa/init?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            $(\"#txtConfigure2FAUsername\").val(sessionData.username);\n            $(\"#lblConfigure2FAStatus\").text(responseJSON.response.totpEnabled ? \"Enabled\" : \"Disabled\");\n\n            if (responseJSON.response.totpEnabled) {\n                $(\"#divConfigure2FAInitialize\").hide();\n\n                divConfigure2FALoader.hide();\n                divConfigure2FAViewer.show();\n\n                btnDisable2FA.show();\n            }\n            else {\n                var secret = \"\";\n\n                for (var i = 0; i < responseJSON.response.secret.length; i++) {\n                    if ((i > 0) && (i % 4) == 0)\n                        secret += \" \";\n\n                    secret += responseJSON.response.secret.substring(i, i + 1);\n                }\n\n                $(\"#lblConfigure2FAQRCode\").html(\"<img src=\\\"data:image/png;base64, \" + responseJSON.response.qrCodePngImage + \"\\\" />\");\n                $(\"#lblConfigure2FASecret\").text(secret);\n                $(\"#txtConfigure2FATOTP\").val(\"\");\n\n                $(\"#divConfigure2FAInitialize\").show();\n\n                divConfigure2FALoader.hide();\n                divConfigure2FAViewer.show();\n\n                btnEnable2FA.show();\n\n                setTimeout(function () {\n                    $(\"#txtConfigure2FATOTP\").trigger(\"focus\");\n                }, 1000);\n            }\n        },\n        error: function () {\n            divConfigure2FALoader.hide();\n        },\n        invalidToken: function () {\n            modalConfigure2FA.modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divConfigure2FAAlert,\n        objLoaderPlaceholder: divConfigure2FALoader\n    });\n}\n\nfunction enable2FA(objBtn) {\n    var btn = $(objBtn);\n\n    var divConfigure2FAAlert = $(\"#divConfigure2FAAlert\");\n    var totp = $(\"#txtConfigure2FATOTP\").val();\n\n    if ((totp == null) || (totp.length != 6)) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter the 6-digit OTP that you see in your authenticator app.\", divConfigure2FAAlert);\n        $(\"#txtConfigure2FATOTP\").trigger(\"focus\");\n        return;\n    }\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/user/2fa/enable?token=\" + sessionData.token + \"&totp=\" + encodeURIComponent(totp),\n        success: function (responseJSON) {\n            sessionData.totpEnabled = true;\n\n            $(\"#modalConfigure2FA\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"2FA Enabled!\", \"Two-factor authentication (2FA) was enabled successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n            $(\"#txtConfigure2FATOTP\").val(\"\");\n            $(\"#txtConfigure2FATOTP\").trigger(\"focus\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalConfigure2FA\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divConfigure2FAAlert\n    });\n}\n\nfunction disable2FA(objBtn) {\n    if (!confirm(\"Are you sure you want to disable Two-factor authentication (2FA) ?\"))\n        return;\n\n    var btn = $(objBtn);\n\n    var divConfigure2FAAlert = $(\"#divConfigure2FAAlert\");\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/user/2fa/disable?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            sessionData.totpEnabled = false;\n\n            $(\"#modalConfigure2FA\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"2FA Disabled!\", \"Two-factor authentication (2FA) was disabled successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalConfigure2FA\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divConfigure2FAAlert\n    });\n}\n\nfunction showMyProfileModal() {\n    var divMyProfileAlert = $(\"#divMyProfileAlert\");\n    var divMyProfileLoader = $(\"#divMyProfileLoader\");\n    var divMyProfileViewer = $(\"#divMyProfileViewer\");\n\n    divMyProfileLoader.show();\n    divMyProfileViewer.hide();\n\n    var modalMyProfile = $(\"#modalMyProfile\");\n    modalMyProfile.modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/user/profile/get?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            sessionData.displayName = responseJSON.response.displayName;\n            sessionData.username = responseJSON.response.username;\n            sessionData.totpEnabled = responseJSON.response.totpEnabled;\n\n            $(\"#mnuUserDisplayName\").text(sessionData.displayName);\n\n            $(\"#txtMyProfileDisplayName\").val(responseJSON.response.displayName);\n            $(\"#txtMyProfileUsername\").val(responseJSON.response.username);\n            $(\"#lblMyProfile2FAStatus\").text(responseJSON.response.totpEnabled ? \"Enabled\" : \"Disabled\");\n            $(\"#txtMyProfileSessionTimeout\").val(responseJSON.response.sessionTimeoutSeconds);\n\n            {\n                var groupHtmlRows = \"\";\n\n                for (var i = 0; i < responseJSON.response.memberOfGroups.length; i++) {\n                    groupHtmlRows += \"<tr><td>\" + htmlEncode(responseJSON.response.memberOfGroups[i]) + \"</td></tr>\";\n                }\n\n                $(\"#tbodyMyProfileMemberOf\").html(groupHtmlRows);\n                $(\"#tfootMyProfileMemberOf\").html(\"Total Groups: \" + responseJSON.response.memberOfGroups.length);\n            }\n\n            {\n                var sessionHtmlRows = \"\";\n\n                for (var i = 0; i < responseJSON.response.sessions.length; i++) {\n                    var session;\n\n                    if (responseJSON.response.sessions[i].tokenName == null)\n                        session = htmlEncode(\"[\" + responseJSON.response.sessions[i].partialToken + \"]\");\n                    else\n                        session = htmlEncode(responseJSON.response.sessions[i].tokenName) + \"<br />[\" + htmlEncode(responseJSON.response.sessions[i].partialToken) + \"]\";\n\n                    if (responseJSON.response.sessions[i].isCurrentSession)\n                        session += \"<br />(current)\";\n\n                    switch (responseJSON.response.sessions[i].type) {\n                        case \"Standard\":\n                            session += \"<br /><span class=\\\"label label-default\\\">Standard</span>\";\n                            break;\n\n                        case \"ApiToken\":\n                            session += \"<br /><span class=\\\"label label-info\\\">API Token</span>\";\n                            break;\n\n                        default:\n                            session += \"<br /><span class=\\\"label label-warning\\\">Unknown</span>\";\n                            break;\n                    }\n\n                    sessionHtmlRows += \"<tr id=\\\"trMyProfileActiveSessions\" + i + \"\\\"><td style=\\\"min-width: 155px; word-wrap: anywhere;\\\">\" + session + \"</td><td>\" +\n                        htmlEncode(moment(responseJSON.response.sessions[i].lastSeen).local().format(\"YYYY-MM-DD HH:mm:ss\")) + \"<br /><span style=\\\"font-size: 12px\\\">\" + htmlEncode(\"(\" + moment(responseJSON.response.sessions[i].lastSeen).fromNow() + \")\") + \"</span></td><td>\" +\n                        htmlEncode(responseJSON.response.sessions[i].lastSeenRemoteAddress) + \"</td><td style=\\\"word-wrap: anywhere;\\\">\" +\n                        htmlEncode(responseJSON.response.sessions[i].lastSeenUserAgent);\n\n                    sessionHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnMyProfileActiveSessionRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                    sessionHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-session-type=\\\"\" + responseJSON.response.sessions[i].type + \"\\\" data-partial-token=\\\"\" + responseJSON.response.sessions[i].partialToken + \"\\\" onclick=\\\"deleteMySession(this); return false;\\\">Delete Session</a></li>\";\n                    sessionHtmlRows += \"</ul></div></td></tr>\";\n                }\n\n                $(\"#tbodyMyProfileActiveSessions\").html(sessionHtmlRows);\n                $(\"#tfootMyProfileActiveSessions\").html(\"Total Sessions: \" + responseJSON.response.sessions.length);\n            }\n\n            divMyProfileLoader.hide();\n            divMyProfileViewer.show();\n\n            setTimeout(function () {\n                $(\"#txtMyProfileDisplayName\").trigger(\"focus\");\n            }, 1000);\n        },\n        error: function () {\n            divMyProfileLoader.hide();\n        },\n        invalidToken: function () {\n            modalMyProfile.modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divMyProfileAlert,\n        objLoaderPlaceholder: divMyProfileLoader\n    });\n}\n\nfunction saveMyProfile(objBtn) {\n    var btn = $(objBtn);\n    var divMyProfileAlert = $(\"#divMyProfileAlert\");\n\n    var displayName = $(\"#txtMyProfileDisplayName\").val();\n\n    var sessionTimeoutSeconds = $(\"#txtMyProfileSessionTimeout\").val();\n    if (sessionTimeoutSeconds === \"\")\n        sessionTimeoutSeconds = 1800;\n\n    var apiUrl = \"api/user/profile/set?token=\" + sessionData.token + \"&displayName=\" + encodeURIComponent(displayName) + \"&sessionTimeoutSeconds=\" + encodeURIComponent(sessionTimeoutSeconds);\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl,\n        success: function (responseJSON) {\n            sessionData.displayName = responseJSON.response.displayName;\n            $(\"#mnuUserDisplayName\").text(sessionData.displayName);\n\n            btn.button(\"reset\");\n            $(\"#modalMyProfile\").modal(\"hide\");\n\n            showAlert(\"success\", \"Profile Saved!\", \"User profile was saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalMyProfile\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divMyProfileAlert\n    });\n}\n\nfunction deleteMySession(objMenuItem) {\n    var divMyProfileAlert = $(\"#divMyProfileAlert\");\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var sessionType = mnuItem.attr(\"data-session-type\");\n    var partialToken = mnuItem.attr(\"data-partial-token\");\n\n    if (!confirm(\"Are you sure you want to delete the session [\" + partialToken + \"] ?\"))\n        return;\n\n    var apiUrl = \"api/user/session/delete?token=\" + sessionData.token + \"&partialToken=\" + encodeURIComponent(partialToken);\n\n    if (sessionType == \"ApiToken\")\n        apiUrl += \"&node=\" + encodeURIComponent(getPrimaryClusterNodeName());\n\n    var btn = $(\"#btnMyProfileActiveSessionRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: apiUrl,\n        success: function (responseJSON) {\n            $(\"#trMyProfileActiveSessions\" + id).remove();\n\n            var totalSessions = $('#tableMyProfileActiveSessions >tbody >tr').length;\n            $(\"#tfootMyProfileActiveSessions\").html(\"Total Sessions: \" + totalSessions);\n\n            showAlert(\"success\", \"Session Deleted!\", \"The user session was deleted successfully.\", divMyProfileAlert);\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            $(\"#modalMyProfile\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divMyProfileAlert\n    });\n}\n\nfunction refreshAdminTab() {\n    if ($(\"#adminTabListSessions\").hasClass(\"active\"))\n        refreshAdminSessions();\n    else if ($(\"#adminTabListUsers\").hasClass(\"active\"))\n        refreshAdminUsers();\n    else if ($(\"#adminTabListGroups\").hasClass(\"active\"))\n        refreshAdminGroups();\n    else if ($(\"#adminTabListPermissions\").hasClass(\"active\"))\n        refreshAdminPermissions();\n    else if ($(\"#adminTabListCluster\").hasClass(\"active\"))\n        refreshAdminCluster();\n    else\n        refreshAdminSessions();\n}\n\nfunction refreshAdminSessions() {\n    var divAdminSessionsLoader = $(\"#divAdminSessionsLoader\");\n    var divAdminSessionsView = $(\"#divAdminSessionsView\");\n\n    var node = $(\"#optAdminSessionsClusterNode\").val();\n\n    divAdminSessionsLoader.show();\n    divAdminSessionsView.hide();\n\n    HTTPRequest({\n        url: \"api/admin/sessions/list?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < responseJSON.response.sessions.length; i++) {\n                var session;\n\n                if (responseJSON.response.sessions[i].tokenName == null)\n                    session = \"[\" + htmlEncode(responseJSON.response.sessions[i].partialToken) + \"]\";\n                else\n                    session = htmlEncode(responseJSON.response.sessions[i].tokenName) + \"<br />[\" + htmlEncode(responseJSON.response.sessions[i].partialToken) + \"]\";\n\n                if (responseJSON.response.sessions[i].isCurrentSession)\n                    session += \"<br />(current)\";\n\n                switch (responseJSON.response.sessions[i].type) {\n                    case \"Standard\":\n                        session += \"<br /><span class=\\\"label label-default\\\">Standard</span>\";\n                        break;\n\n                    case \"ApiToken\":\n                        session += \"<br /><span class=\\\"label label-info\\\">API Token</span>\";\n                        break;\n\n                    default:\n                        session += \"<br /><span class=\\\"label label-warning\\\">Unknown</span>\";\n                        break;\n                }\n\n                tableHtmlRows += \"<tr id=\\\"trAdminSessions\" + i + \"\\\"><td>\" + htmlEncode(responseJSON.response.sessions[i].username) + \"</td><td style=\\\"min-width: 155px; word-wrap: anywhere;\\\">\" +\n                    session + \"</td><td>\" +\n                    htmlEncode(moment(responseJSON.response.sessions[i].lastSeen).local().format(\"YYYY-MM-DD HH:mm:ss\")) + \"<br /><span style=\\\"font-size: 12px\\\">\" + htmlEncode(\"(\" + moment(responseJSON.response.sessions[i].lastSeen).fromNow() + \")\") + \"</span></td><td>\" +\n                    htmlEncode(responseJSON.response.sessions[i].lastSeenRemoteAddress) + \"</td><td style=\\\"word-wrap: anywhere;\\\">\" +\n                    htmlEncode(responseJSON.response.sessions[i].lastSeenUserAgent);\n\n                tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnAdminSessionRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-session-type=\\\"\" + responseJSON.response.sessions[i].type + \"\\\" data-partial-token=\\\"\" + responseJSON.response.sessions[i].partialToken + \"\\\" onclick=\\\"deleteAdminSession(this); return false;\\\">Delete Session</a></li>\";\n                tableHtmlRows += \"</ul></div></td></tr>\";\n            }\n\n            var primaryNodeName = getPrimaryClusterNodeName();\n\n            if ((primaryNodeName == \"\") || (primaryNodeName == responseJSON.server))\n                $(\"#btnAdminSessionsCreateToken\").show();\n            else\n                $(\"#btnAdminSessionsCreateToken\").hide();\n\n            $(\"#tbodyAdminSessions\").html(tableHtmlRows);\n            $(\"#tfootAdminSessions\").html(\"Total Sessions: \" + responseJSON.response.sessions.length);\n\n            divAdminSessionsLoader.hide();\n            divAdminSessionsView.show();\n        },\n        error: function () {\n            divAdminSessionsLoader.hide();\n            divAdminSessionsView.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divAdminSessionsLoader\n    });\n}\n\nfunction showCreateApiTokenModal() {\n    var divCreateApiTokenAlert = $(\"#divCreateApiTokenAlert\");\n    var divCreateApiTokenLoader = $(\"#divCreateApiTokenLoader\");\n    var divCreateApiTokenForm = $(\"#divCreateApiTokenForm\");\n    var divCreateApiTokenOutput = $(\"#divCreateApiTokenOutput\");\n\n    divCreateApiTokenLoader.show();\n    divCreateApiTokenForm.hide();\n    divCreateApiTokenOutput.hide();\n\n    var btnCreateApiToken = $(\"#btnCreateApiToken\");\n    btnCreateApiToken.attr(\"onclick\", \"createApiToken(this); return false;\");\n    btnCreateApiToken.show();\n\n    var modalCreateApiToken = $(\"#modalCreateApiToken\");\n    modalCreateApiToken.modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/users/list?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var userListHtml = \"\";\n\n            for (var i = 0; i < responseJSON.response.users.length; i++) {\n                userListHtml += \"<option>\" + htmlEncode(responseJSON.response.users[i].username) + \"</option>\";\n            }\n\n            $(\"#optCreateApiTokenUsername\").html(userListHtml);\n\n            $(\"#optCreateApiTokenUsername\").show();\n            $(\"#txtCreateApiTokenUsername\").hide();\n            $(\"#divCreateApiTokenPassword\").hide();\n            $(\"#divCreateApiToken2FAOTP\").hide();\n            $(\"#txtCreateApiTokenName\").val(\"\");\n\n            divCreateApiTokenLoader.hide();\n            divCreateApiTokenForm.show();\n\n            setTimeout(function () {\n                $(\"#optCreateApiTokenUsername\").trigger(\"focus\");\n            }, 1000);\n        },\n        error: function () {\n            divCreateApiTokenLoader.hide();\n        },\n        invalidToken: function () {\n            modalCreateApiToken.modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divCreateApiTokenAlert,\n        objLoaderPlaceholder: divCreateApiTokenLoader\n    });\n}\n\nfunction createApiToken(objBtn) {\n    var btn = $(objBtn);\n\n    var divCreateApiTokenAlert = $(\"#divCreateApiTokenAlert\");\n\n    var user = $(\"#optCreateApiTokenUsername\").val();\n    var tokenName = $(\"#txtCreateApiTokenName\").val();\n\n    if (user === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please select a username.\", divCreateApiTokenAlert);\n        $(\"#optCreateApiTokenUsername\").trigger(\"focus\");\n        return;\n    }\n\n    if (tokenName === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a token name.\", divCreateApiTokenAlert);\n        $(\"#txtCreateApiTokenName\").trigger(\"focus\");\n        return;\n    }\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/sessions/createToken?token=\" + sessionData.token + \"&user=\" + encodeURIComponent(user) + \"&tokenName=\" + encodeURIComponent(tokenName),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            btn.hide();\n\n            $(\"#lblCreateApiTokenOutputUsername\").text(responseJSON.response.username);\n            $(\"#lblCreateApiTokenOutputTokenName\").text(responseJSON.response.tokenName);\n            $(\"#lblCreateApiTokenOutputToken\").text(responseJSON.response.token);\n\n            $(\"#divCreateApiTokenForm\").hide();\n            $(\"#divCreateApiTokenOutput\").show();\n\n            showAlert(\"success\", \"Token Created!\", \"API token was created successfully.\", divCreateApiTokenAlert);\n\n            refreshAdminSessions();\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalCreateApiToken\").hide(\"\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divCreateApiTokenAlert\n    });\n}\n\nfunction deleteAdminSession(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var sessionType = mnuItem.attr(\"data-session-type\");\n    var partialToken = mnuItem.attr(\"data-partial-token\");\n\n    if (!confirm(\"Are you sure you want to delete the session [\" + partialToken + \"] ?\"))\n        return;\n\n    var apiUrl = \"api/admin/sessions/delete?token=\" + sessionData.token + \"&partialToken=\" + encodeURIComponent(partialToken);\n\n    if (sessionType == \"ApiToken\")\n        apiUrl += \"&node=\" + encodeURIComponent(getPrimaryClusterNodeName());\n    else\n        apiUrl += \"&node=\" + encodeURIComponent($(\"#optAdminSessionsClusterNode\").val());\n\n    var btn = $(\"#btnAdminSessionRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: apiUrl,\n        success: function (responseJSON) {\n            $(\"#trAdminSessions\" + id).remove();\n\n            var totalSessions = $('#tableAdminSessions >tbody >tr').length;\n            $(\"#tfootAdminSessions\").html(\"Total Sessions: \" + totalSessions);\n\n            showAlert(\"success\", \"Session Deleted!\", \"The user session was deleted successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction refreshAdminUsers() {\n    var divAdminUsersLoader = $(\"#divAdminUsersLoader\");\n    var divAdminUsersView = $(\"#divAdminUsersView\");\n\n    divAdminUsersLoader.show();\n    divAdminUsersView.hide();\n\n    HTTPRequest({\n        url: \"api/admin/users/list?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < responseJSON.response.users.length; i++) {\n                tableHtmlRows += getAdminUsersRowHtml(i, responseJSON.response.users[i]);\n            }\n\n            $(\"#tbodyAdminUsers\").html(tableHtmlRows);\n            $(\"#tfootAdminUsers\").html(\"Total Users: \" + responseJSON.response.users.length);\n\n            divAdminUsersLoader.hide();\n            divAdminUsersView.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divAdminUsersLoader\n    });\n}\n\nfunction getAdminUsersRowHtml(id, user) {\n    var totpStatus = \"\";\n    if (user.totpEnabled)\n        totpStatus += \"<span class=\\\"label label-success\\\">Enabled</span>\";\n    else\n        totpStatus += \"<span class=\\\"label label-default\\\">Disabled</span>\";\n\n    var status = \"\";\n    if (user.disabled)\n        status += \"<span class=\\\"label label-warning\\\">Disabled</span>\";\n    else\n        status += \"<span class=\\\"label label-success\\\">Enabled</span>\";\n\n    var tableHtmlRows = \"<tr id=\\\"trAdminUsers\" + id + \"\\\"><td style=\\\"word-wrap: anywhere;\\\"><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-username=\\\"\" + htmlEncode(user.username) + \"\\\" onclick=\\\"showUserDetailsModal(this); return false;\\\">\" + htmlEncode(user.username) + \"</a></td><td style=\\\"word-wrap: anywhere;\\\">\" +\n        htmlEncode(user.displayName) + \"</td><td>\" +\n        totpStatus + \"</td><td>\" +\n        status + \"</td><td>\" +\n        htmlEncode(moment(user.recentSessionLoggedOn).local().format(\"YYYY-MM-DD HH:mm:ss\")) + \" from \" + htmlEncode(user.recentSessionRemoteAddress) + \"</td><td>\" +\n        htmlEncode(moment(user.previousSessionLoggedOn).local().format(\"YYYY-MM-DD HH:mm:ss\")) + \" from \" + htmlEncode(user.previousSessionRemoteAddress);\n\n    tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnAdminUserRowOption\" + id + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-username=\\\"\" + htmlEncode(user.username) + \"\\\" onclick=\\\"showUserDetailsModal(this); return false;\\\">View Details</a></li>\";\n    tableHtmlRows += \"<li id=\\\"mnuAdminUserRowEnable\" + id + \"\\\"\" + (user.disabled ? \"\" : \" style=\\\"display: none;\\\"\") + \"><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-username=\\\"\" + htmlEncode(user.username) + \"\\\" onclick=\\\"enableUser(this); return false;\\\">Enable</a></li>\";\n    tableHtmlRows += \"<li id=\\\"mnuAdminUserRowDisable\" + id + \"\\\"\" + (!user.disabled ? \"\" : \" style=\\\"display: none;\\\"\") + \"><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-username=\\\"\" + htmlEncode(user.username) + \"\\\" onclick=\\\"disableUser(this); return false;\\\">Disable</a></li>\";\n    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-username=\\\"\" + htmlEncode(user.username) + \"\\\" onclick=\\\"showResetUserPasswordModal(this); return false;\\\">Reset Password</a></li>\";\n\n    if (user.totpEnabled)\n        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-username=\\\"\" + htmlEncode(user.username) + \"\\\" onclick=\\\"adminDisable2FA(this); return false;\\\">Disable 2FA</a></li>\";\n\n    tableHtmlRows += \"<li role=\\\"separator\\\" class=\\\"divider\\\"></li>\";\n    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-username=\\\"\" + htmlEncode(user.username) + \"\\\" onclick=\\\"deleteUser(this); return false;\\\">Delete User</a></li>\";\n    tableHtmlRows += \"</ul></div></td></tr>\";\n\n    return tableHtmlRows;\n}\n\nfunction showAddUserModal() {\n    $(\"#divAddUserAlert\").html(\"\");\n\n    $(\"#txtAddUserDisplayName\").val(\"\");\n    $(\"#txtAddUserUsername\").val(\"\");\n    $(\"#txtAddUserPassword\").val(\"\");\n    $(\"#txtAddUserConfirmPassword\").val(\"\");\n\n    $(\"#modalAddUser\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtAddUserDisplayName\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction addUser(objBtn) {\n    var btn = $(objBtn);\n    var divAddUserAlert = $(\"#divAddUserAlert\");\n\n    var user = $(\"#txtAddUserUsername\").val();\n    if (user === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter an username to add user.\", divAddUserAlert);\n        $(\"#txtAddUserUsername\").trigger(\"focus\");\n        return;\n    }\n\n    var pass = $(\"#txtAddUserPassword\").val();\n    if (pass === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a password to add user.\", divAddUserAlert);\n        $(\"#txtAddUserPassword\").trigger(\"focus\");\n        return;\n    }\n\n    var confirmPass = $(\"#txtAddUserConfirmPassword\").val();\n    if (confirmPass === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter confirm password.\", divAddUserAlert);\n        $(\"#txtAddUserConfirmPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if (pass !== confirmPass) {\n        showAlert(\"warning\", \"Mismatch!\", \"Passwords do not match. Please try again.\", divAddUserAlert);\n        $(\"#txtAddUserConfirmPassword\").trigger(\"focus\");\n        return;\n    }\n\n    var displayName = $(\"#txtAddUserDisplayName\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/users/create\",\n        method: \"POST\",\n        data: \"token=\" + sessionData.token + \"&displayName=\" + encodeURIComponent(displayName) + \"&user=\" + encodeURIComponent(user) + \"&pass=\" + encodeURIComponent(pass),\n        processData: false,\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalAddUser\").modal(\"hide\");\n\n            var id = Math.floor(Math.random() * 1000000);\n            var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response);\n            $(\"#tableAdminUsers\").prepend(tableHtmlRow);\n\n            var totalUsers = $('#tableAdminUsers >tbody >tr').length;\n            $(\"#tfootAdminUsers\").html(\"Total Users: \" + totalUsers);\n\n            showAlert(\"success\", \"User Added!\", \"User was added successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalAddUser\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAddUserAlert\n    });\n}\n\nfunction showUserDetailsModal(objMenuItem) {\n    var divUserDetailsAlert = $(\"#divUserDetailsAlert\");\n    var divUserDetailsLoader = $(\"#divUserDetailsLoader\");\n    var divUserDetailsViewer = $(\"#divUserDetailsViewer\");\n\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var username = mnuItem.attr(\"data-username\");\n\n    divUserDetailsLoader.show();\n    divUserDetailsViewer.hide();\n\n    var modalUserDetails = $(\"#modalUserDetails\");\n    modalUserDetails.modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/users/get?token=\" + sessionData.token + \"&user=\" + encodeURIComponent(username) + \"&includeGroups=true\",\n        success: function (responseJSON) {\n            $(\"#txtUserDetailsDisplayName\").val(responseJSON.response.displayName);\n            $(\"#txtUserDetailsUsername\").val(responseJSON.response.username);\n            $(\"#lblUserDetails2FAStatus\").text(responseJSON.response.totpEnabled ? \"Enabled\" : \"Disabled\");\n            $(\"#chkUserDetailsDisableAccount\").prop(\"checked\", responseJSON.response.disabled);\n            $(\"#txtUserDetailsSessionTimeout\").val(responseJSON.response.sessionTimeoutSeconds);\n\n            var memberOf = \"\";\n\n            for (var i = 0; i < responseJSON.response.memberOfGroups.length; i++) {\n                memberOf += htmlEncode(responseJSON.response.memberOfGroups[i]) + \"\\n\";\n            }\n\n            $(\"#txtUserDetailsMemberOf\").val(memberOf);\n\n            var groupListHtml = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n            for (var i = 0; i < responseJSON.response.groups.length; i++) {\n                groupListHtml += \"<option>\" + htmlEncode(responseJSON.response.groups[i]) + \"</option>\";\n            }\n\n            $(\"#optUserDetailsGroupList\").html(groupListHtml);\n\n            var sessionHtmlRows = \"\";\n\n            for (var i = 0; i < responseJSON.response.sessions.length; i++) {\n                var session;\n\n                if (responseJSON.response.sessions[i].tokenName == null)\n                    session = htmlEncode(\"[\" + responseJSON.response.sessions[i].partialToken + \"]\");\n                else\n                    session = htmlEncode(responseJSON.response.sessions[i].tokenName) + \"<br />[\" + htmlEncode(responseJSON.response.sessions[i].partialToken) + \"]\";\n\n                if (responseJSON.response.sessions[i].isCurrentSession)\n                    session += \"<br />(current)\";\n\n                switch (responseJSON.response.sessions[i].type) {\n                    case \"Standard\":\n                        session += \"<br /><span class=\\\"label label-default\\\">Standard</span>\";\n                        break;\n\n                    case \"ApiToken\":\n                        session += \"<br /><span class=\\\"label label-info\\\">API Token</span>\";\n                        break;\n\n                    default:\n                        session += \"<br /><span class=\\\"label label-warning\\\">Unknown</span>\";\n                        break;\n                }\n\n                sessionHtmlRows += \"<tr id=\\\"trUserDetailsActiveSessions\" + i + \"\\\"><td style=\\\"min-width: 155px; word-wrap: anywhere;\\\">\" + session + \"</td><td>\" +\n                    htmlEncode(moment(responseJSON.response.sessions[i].lastSeen).local().format(\"YYYY-MM-DD HH:mm:ss\")) + \"<br /><span style=\\\"font-size: 12px\\\">\" + htmlEncode(\"(\" + moment(responseJSON.response.sessions[i].lastSeen).fromNow() + \")\") + \"</span></td><td>\" +\n                    htmlEncode(responseJSON.response.sessions[i].lastSeenRemoteAddress) + \"</td><td style=\\\"word-wrap: anywhere;\\\">\" +\n                    htmlEncode(responseJSON.response.sessions[i].lastSeenUserAgent);\n\n                sessionHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnUserDetailsActiveSessionRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                sessionHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-session-type=\\\"\" + responseJSON.response.sessions[i].type + \"\\\" data-partial-token=\\\"\" + responseJSON.response.sessions[i].partialToken + \"\\\" onclick=\\\"deleteUserSession(this); return false;\\\">Delete Session</a></li>\";\n                sessionHtmlRows += \"</ul></div></td></tr>\";\n            }\n\n            $(\"#tbodyUserDetailsActiveSessions\").html(sessionHtmlRows);\n            $(\"#tfootUserDetailsActiveSessions\").html(\"Total Sessions: \" + responseJSON.response.sessions.length);\n\n            var btnUserDetailsSave = $(\"#btnUserDetailsSave\");\n            btnUserDetailsSave.attr(\"data-id\", id);\n            btnUserDetailsSave.attr(\"data-username\", username);\n\n            divUserDetailsLoader.hide();\n            divUserDetailsViewer.show();\n\n            setTimeout(function () {\n                $(\"#txtUserDetailsDisplayName\").trigger(\"focus\");\n            }, 1000);\n        },\n        error: function () {\n            divUserDetailsLoader.hide();\n        },\n        invalidToken: function () {\n            modalUserDetails.modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divUserDetailsAlert,\n        objLoaderPlaceholder: divUserDetailsLoader\n    });\n}\n\nfunction deleteUserSession(objMenuItem) {\n    var divUserDetailsAlert = $(\"#divUserDetailsAlert\");\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var sessionType = mnuItem.attr(\"data-session-type\");\n    var partialToken = mnuItem.attr(\"data-partial-token\");\n\n    if (!confirm(\"Are you sure you want to delete the session [\" + partialToken + \"] ?\"))\n        return;\n\n    var apiUrl = \"api/admin/sessions/delete?token=\" + sessionData.token + \"&partialToken=\" + encodeURIComponent(partialToken);\n\n    if (sessionType == \"ApiToken\")\n        apiUrl += \"&node=\" + encodeURIComponent(getPrimaryClusterNodeName());\n\n    var btn = $(\"#btnUserDetailsActiveSessionRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: apiUrl,\n        success: function (responseJSON) {\n            $(\"#trUserDetailsActiveSessions\" + id).remove();\n\n            var totalSessions = $('#tableUserDetailsActiveSessions >tbody >tr').length;\n            $(\"#tfootUserDetailsActiveSessions\").html(\"Total Sessions: \" + totalSessions);\n\n            showAlert(\"success\", \"Session Deleted!\", \"The user session was deleted successfully.\", divUserDetailsAlert);\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            $(\"#modalUserDetails\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divUserDetailsAlert\n    });\n}\n\nfunction saveUserDetails(objBtn) {\n    var btn = $(objBtn);\n    var divUserDetailsAlert = $(\"#divUserDetailsAlert\");\n\n    var id = btn.attr(\"data-id\");\n    var username = btn.attr(\"data-username\");\n    var newUsername = $(\"#txtUserDetailsUsername\").val();\n    var displayName = $(\"#txtUserDetailsDisplayName\").val();\n    var disabled = $(\"#chkUserDetailsDisableAccount\").prop(\"checked\");\n\n    var sessionTimeoutSeconds = $(\"#txtUserDetailsSessionTimeout\").val();\n    if (sessionTimeoutSeconds === \"\")\n        sessionTimeoutSeconds = 1800;\n\n    var memberOfGroups = cleanTextList($(\"#txtUserDetailsMemberOf\").val());\n\n    var apiUrl = \"api/admin/users/set?token=\" + sessionData.token + \"&user=\" + encodeURIComponent(username) + \"&displayName=\" + encodeURIComponent(displayName) + \"&disabled=\" + disabled + \"&sessionTimeoutSeconds=\" + encodeURIComponent(sessionTimeoutSeconds) + \"&memberOfGroups=\" + encodeURIComponent(memberOfGroups);\n\n    if (newUsername !== username)\n        apiUrl += \"&newUser=\" + encodeURIComponent(newUsername);\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl,\n        success: function (responseJSON) {\n            if (sessionData.username === username) {\n                sessionData.displayName = responseJSON.response.displayName;\n                sessionData.username = responseJSON.response.username;\n                $(\"#mnuUserDisplayName\").text(sessionData.displayName);\n            }\n\n            var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response);\n            $(\"#trAdminUsers\" + id).replaceWith(tableHtmlRow);\n\n            btn.button(\"reset\");\n            $(\"#modalUserDetails\").modal(\"hide\");\n\n            showAlert(\"success\", \"User Saved!\", \"User details were saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalUserDetails\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divUserDetailsAlert\n    });\n}\n\nfunction disableUser(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var username = mnuItem.attr(\"data-username\");\n\n    if (!confirm(\"Are you sure you want to disable the user [\" + username + \"] account?\"))\n        return;\n\n    var btn = $(\"#btnAdminUserRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/admin/users/set?token=\" + sessionData.token + \"&user=\" + encodeURIComponent(username) + \"&disabled=true\",\n        success: function (responseJSON) {\n            var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response);\n            $(\"#trAdminUsers\" + id).replaceWith(tableHtmlRow);\n\n            showAlert(\"success\", \"User Disabled!\", \"User [\" + username + \"] account was disabled successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction enableUser(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var username = mnuItem.attr(\"data-username\");\n\n    var btn = $(\"#btnAdminUserRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/admin/users/set?token=\" + sessionData.token + \"&user=\" + encodeURIComponent(username) + \"&disabled=false\",\n        success: function (responseJSON) {\n            var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response);\n            $(\"#trAdminUsers\" + id).replaceWith(tableHtmlRow);\n\n            showAlert(\"success\", \"User Enabled!\", \"User [\" + username + \"] account was enabled successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction showResetUserPasswordModal(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var username = mnuItem.attr(\"data-username\");\n\n    $(\"#titleChangePassword\").text(\"Reset Password\");\n\n    hideAlert($(\"#divChangePasswordAlert\"));\n    $(\"#txtChangePasswordUsername\").val(username);\n    $(\"#divChangePasswordCurrentPassword\").hide();\n    $(\"#txtChangePasswordNewPassword\").val(\"\");\n    $(\"#txtChangePasswordConfirmPassword\").val(\"\");\n    $(\"#divChangePassword2FATOTP\").hide();\n\n    var btnChangePassword = $(\"#btnChangePassword\");\n    btnChangePassword.text(\"Reset\");\n    btnChangePassword.attr(\"onclick\", \"resetUserPassword(this); return false;\");\n    btnChangePassword.show();\n\n    $(\"#modalChangePassword\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtChangePasswordNewPassword\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction resetUserPassword(objBtn) {\n    var btn = $(objBtn);\n\n    var divChangePasswordAlert = $(\"#divChangePasswordAlert\");\n\n    var user = $(\"#txtChangePasswordUsername\").val();\n    var newPassword = $(\"#txtChangePasswordNewPassword\").val();\n    var confirmPassword = $(\"#txtChangePasswordConfirmPassword\").val();\n\n    if (newPassword === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter new password.\", divChangePasswordAlert);\n        $(\"#txtChangePasswordNewPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if (confirmPassword === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter confirm password.\", divChangePasswordAlert);\n        $(\"#txtChangePasswordConfirmPassword\").trigger(\"focus\");\n        return;\n    }\n\n    if (newPassword !== confirmPassword) {\n        showAlert(\"warning\", \"Mismatch!\", \"Passwords do not match. Please try again.\", divChangePasswordAlert);\n        $(\"#txtChangePasswordNewPassword\").trigger(\"focus\");\n        return;\n    }\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/users/set\",\n        method: \"POST\",\n        data: \"token=\" + sessionData.token + \"&user=\" + encodeURIComponent(user) + \"&newPass=\" + encodeURIComponent(newPassword),\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalChangePassword\").modal(\"hide\");\n            $(\"#txtChangePasswordCurrentPassword\").val(\"\");\n            $(\"#txtChangePasswordNewPassword\").val(\"\");\n            $(\"#txtChangePasswordConfirmPassword\").val(\"\");\n            $(\"#txtChangePassword2FATOTP\").val(\"\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Password Reset!\", \"Password was reset successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divChangePasswordAlert\n    });\n}\n\nfunction adminDisable2FA(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var username = mnuItem.attr(\"data-username\");\n\n    if (!confirm(\"Are you sure you want to disable Two-factor authentication (2FA) for user [\" + username + \"] ?\"))\n        return;\n\n    var btn = $(\"#btnAdminUserRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/admin/users/set?token=\" + sessionData.token + \"&user=\" + encodeURIComponent(username) + \"&totpEnabled=false\",\n        success: function (responseJSON) {\n            if (username == sessionData.username)\n                sessionData.totpEnabled = false;\n\n            var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response);\n            $(\"#trAdminUsers\" + id).replaceWith(tableHtmlRow);\n\n            showAlert(\"success\", \"2FA Disabled!\", \"Two-factor authentication was disabled successfully for user [\" + username + \"].\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteUser(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var username = mnuItem.attr(\"data-username\");\n\n    if (!confirm(\"Are you sure you want to delete the user [\" + username + \"] account?\"))\n        return;\n\n    var btn = $(\"#btnAdminUserRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/admin/users/delete?token=\" + sessionData.token + \"&user=\" + encodeURIComponent(username),\n        success: function (responseJSON) {\n            $(\"#trAdminUsers\" + id).remove();\n\n            var totalUsers = $('#tableAdminUsers >tbody >tr').length;\n            $(\"#tfootAdminUsers\").html(\"Total Users: \" + totalUsers);\n\n            showAlert(\"success\", \"User Deleted!\", \"User account was deleted successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction refreshAdminGroups() {\n    var divAdminGroupsLoader = $(\"#divAdminGroupsLoader\");\n    var divAdminGroupsView = $(\"#divAdminGroupsView\");\n\n    divAdminGroupsLoader.show();\n    divAdminGroupsView.hide();\n\n    HTTPRequest({\n        url: \"api/admin/groups/list?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < responseJSON.response.groups.length; i++) {\n                tableHtmlRows += getAdminGroupsRowHtml(i, responseJSON.response.groups[i]);\n            }\n\n            $(\"#tbodyAdminGroups\").html(tableHtmlRows);\n            $(\"#tfootAdminGroups\").html(\"Total Groups: \" + responseJSON.response.groups.length);\n\n            divAdminGroupsLoader.hide();\n            divAdminGroupsView.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divAdminGroupsLoader\n    });\n}\n\nfunction getAdminGroupsRowHtml(id, group) {\n    var tableHtmlRows = \"<tr id=\\\"trAdminGroups\" + id + \"\\\"><td style=\\\"word-wrap: anywhere;\\\"><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-group=\\\"\" + htmlEncode(group.name) + \"\\\" onclick=\\\"showGroupDetailsModal(this); return false;\\\">\" + htmlEncode(group.name) + \"</a></td><td style=\\\"word-wrap: anywhere;\\\">\" +\n        htmlEncode(group.description).replace(/\\n/g, \"<br />\");\n\n    tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnAdminGroupRowOption\" + id + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-group=\\\"\" + htmlEncode(group.name) + \"\\\" onclick=\\\"showGroupDetailsModal(this); return false;\\\">View Details</a></li>\";\n    tableHtmlRows += \"<li role=\\\"separator\\\" class=\\\"divider\\\"></li>\";\n    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-group=\\\"\" + htmlEncode(group.name) + \"\\\" onclick=\\\"deleteGroup(this); return false;\\\">Delete Group</a></li>\";\n    tableHtmlRows += \"</ul></div></td></tr>\";\n\n    return tableHtmlRows;\n}\n\nfunction showAddGroupModal() {\n    $(\"#divAddGroupAlert\").html(\"\");\n\n    $(\"#txtAddGroupName\").val(\"\");\n    $(\"#txtAddGroupDescription\").val(\"\");\n\n    $(\"#modalAddGroup\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtAddGroupName\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction addGroup(objBtn) {\n    var btn = $(objBtn);\n    var divAddGroupAlert = $(\"#divAddGroupAlert\");\n\n    var group = $(\"#txtAddGroupName\").val();\n    if (group === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a name to add group.\", divAddGroupAlert);\n        $(\"#txtAddGroupName\").trigger(\"focus\");\n        return;\n    }\n\n    var description = $(\"#txtAddGroupDescription\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/groups/create?token=\" + sessionData.token + \"&group=\" + encodeURIComponent(group) + \"&description=\" + encodeURIComponent(description),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalAddGroup\").modal(\"hide\");\n\n            var id = Math.floor(Math.random() * 1000000);\n            var tableHtmlRow = getAdminGroupsRowHtml(id, responseJSON.response);\n            $(\"#tableAdminGroups\").prepend(tableHtmlRow);\n\n            var totalGroups = $('#tableAdminGroups >tbody >tr').length;\n            $(\"#tfootAdminGroups\").html(\"Total Groups: \" + totalGroups);\n\n            showAlert(\"success\", \"Group Added!\", \"Group was added successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalAddGroup\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAddGroupAlert\n    });\n}\n\nfunction showGroupDetailsModal(objMenuItem) {\n    var divGroupDetailsAlert = $(\"#divGroupDetailsAlert\");\n    var divGroupDetailsLoader = $(\"#divGroupDetailsLoader\");\n    var divGroupDetailsViewer = $(\"#divGroupDetailsViewer\");\n\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var group = mnuItem.attr(\"data-group\");\n\n    divGroupDetailsLoader.show();\n    divGroupDetailsViewer.hide();\n\n    var modalGroupDetails = $(\"#modalGroupDetails\");\n    modalGroupDetails.modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/groups/get?token=\" + sessionData.token + \"&group=\" + encodeURIComponent(group) + \"&includeUsers=true\",\n        success: function (responseJSON) {\n            $(\"#txtGroupDetailsName\").val(responseJSON.response.name);\n            $(\"#txtGroupDetailsDescription\").val(responseJSON.response.description);\n\n            var members = \"\";\n\n            for (var i = 0; i < responseJSON.response.members.length; i++) {\n                members += htmlEncode(responseJSON.response.members[i]) + \"\\n\";\n            }\n\n            $(\"#txtGroupDetailsMembers\").val(members);\n\n            var userListHtml = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n            for (var i = 0; i < responseJSON.response.users.length; i++) {\n                userListHtml += \"<option>\" + htmlEncode(responseJSON.response.users[i]) + \"</option>\";\n            }\n\n            $(\"#optGroupDetailsUserList\").html(userListHtml);\n\n            var btnGroupDetailsSave = $(\"#btnGroupDetailsSave\");\n            btnGroupDetailsSave.attr(\"data-id\", id);\n            btnGroupDetailsSave.attr(\"data-group\", group);\n\n            divGroupDetailsLoader.hide();\n            divGroupDetailsViewer.show();\n\n            setTimeout(function () {\n                $(\"#txtGroupDetailsName\").trigger(\"focus\");\n            }, 1000);\n        },\n        error: function () {\n            divGroupDetailsLoader.hide();\n        },\n        invalidToken: function () {\n            modalGroupDetails.modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divGroupDetailsAlert,\n        objLoaderPlaceholder: divGroupDetailsLoader\n    });\n}\n\nfunction saveGroupDetails(objBtn) {\n    var btn = $(objBtn);\n    var divGroupDetailsAlert = $(\"#divGroupDetailsAlert\");\n\n    var id = btn.attr(\"data-id\");\n    var group = btn.attr(\"data-group\");\n\n    var newGroup = $(\"#txtGroupDetailsName\").val();\n    var description = $(\"#txtGroupDetailsDescription\").val();\n\n    var members = cleanTextList($(\"#txtGroupDetailsMembers\").val());\n\n    var apiUrl = \"api/admin/groups/set?token=\" + sessionData.token + \"&group=\" + encodeURIComponent(group) + \"&description=\" + encodeURIComponent(description) + \"&members=\" + encodeURIComponent(members);\n\n    if (newGroup !== group)\n        apiUrl += \"&newGroup=\" + encodeURIComponent(newGroup);\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl,\n        success: function (responseJSON) {\n            var tableHtmlRow = getAdminGroupsRowHtml(id, responseJSON.response);\n            $(\"#trAdminGroups\" + id).replaceWith(tableHtmlRow);\n\n            btn.button(\"reset\");\n            $(\"#modalGroupDetails\").modal(\"hide\");\n\n            showAlert(\"success\", \"Group Saved!\", \"Group details were saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalGroupDetails\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divGroupDetailsAlert\n    });\n}\n\nfunction deleteGroup(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var group = mnuItem.attr(\"data-group\");\n\n    if (!confirm(\"Are you sure you want to delete the group [\" + group + \"] ?\"))\n        return;\n\n    var btn = $(\"#btnAdminGroupRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/admin/groups/delete?token=\" + sessionData.token + \"&group=\" + encodeURIComponent(group),\n        success: function (responseJSON) {\n            $(\"#trAdminGroups\" + id).remove();\n\n            var totalGroups = $('#tableAdminGroups >tbody >tr').length;\n            $(\"#tfootAdminGroups\").html(\"Total Groups: \" + totalGroups);\n\n            showAlert(\"success\", \"Group Deleted!\", \"Group was deleted successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction refreshAdminPermissions() {\n    var divAdminPermissionsLoader = $(\"#divAdminPermissionsLoader\");\n    var divAdminPermissionsView = $(\"#divAdminPermissionsView\");\n\n    divAdminPermissionsLoader.show();\n    divAdminPermissionsView.hide();\n\n    HTTPRequest({\n        url: \"api/admin/permissions/list?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < responseJSON.response.permissions.length; i++) {\n                tableHtmlRows += getAdminPermissionsRowHtml(i, responseJSON.response.permissions[i]);\n            }\n\n            $(\"#tbodyAdminPermissions\").html(tableHtmlRows);\n            $(\"#tfootAdminPermissions\").html(\"Total Sections: \" + responseJSON.response.permissions.length);\n\n            divAdminPermissionsLoader.hide();\n            divAdminPermissionsView.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divAdminPermissionsLoader\n    });\n}\n\nfunction getAdminPermissionsRowHtml(id, permission) {\n    var userPermissionsHtml = \"<table class=\\\"table\\\" style=\\\"background: transparent;\\\"><thead><tr><th>Username</th><th style=\\\"width: 70px;\\\">View</th><th style=\\\"width: 70px;\\\">Modify</th><th style=\\\"width: 70px;\\\">Delete</th></tr></thead><tbody>\";\n\n    for (var i = 0; i < permission.userPermissions.length; i++) {\n        userPermissionsHtml += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(permission.userPermissions[i].username) + \"</td><td>\" +\n            (permission.userPermissions[i].canView ? \"<span class=\\\"glyphicon glyphicon-ok\\\"></span>\" : \"\") + \"</td><td>\" +\n            (permission.userPermissions[i].canModify ? \"<span class=\\\"glyphicon glyphicon-ok\\\"></span>\" : \"\") + \"</td><td>\" +\n            (permission.userPermissions[i].canDelete ? \"<span class=\\\"glyphicon glyphicon-ok\\\"></span>\" : \"\") + \"</td></tr>\";\n    }\n\n    userPermissionsHtml += \"</tbody>\";\n\n    if (permission.userPermissions.length == 0)\n        userPermissionsHtml += \"<tfoot><tr><th colspan=\\\"4\\\" style=\\\"text-align: center;\\\">No user permissions</th></tfoot>\";\n\n    userPermissionsHtml += \"</table>\";\n\n    var groupPermissionsHtml = \"<table class=\\\"table\\\" style=\\\"background: transparent;\\\"><thead><tr><th>Group</th><th style=\\\"width: 70px;\\\">View</th><th style=\\\"width: 70px;\\\">Modify</th><th style=\\\"width: 70px;\\\">Delete</th></tr></thead><tbody>\";\n\n    for (var i = 0; i < permission.groupPermissions.length; i++) {\n        groupPermissionsHtml += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(permission.groupPermissions[i].name) + \"</td><td>\" +\n            (permission.groupPermissions[i].canView ? \"<span class=\\\"glyphicon glyphicon-ok\\\"></span>\" : \"\") + \"</td><td>\" +\n            (permission.groupPermissions[i].canModify ? \"<span class=\\\"glyphicon glyphicon-ok\\\"></span>\" : \"\") + \"</td><td>\" +\n            (permission.groupPermissions[i].canDelete ? \"<span class=\\\"glyphicon glyphicon-ok\\\"></span>\" : \"\") + \"</td></tr>\";\n    }\n\n    groupPermissionsHtml += \"</tbody>\";\n\n    if (permission.groupPermissions.length == 0)\n        groupPermissionsHtml += \"<tfoot><tr><th colspan=\\\"4\\\" style=\\\"text-align: center;\\\">No group permissions</th></tfoot>\";\n\n    groupPermissionsHtml += \"</table>\";\n\n    var tableHtmlRows = \"<tr id=\\\"trAdminPermissions\" + id + \"\\\"><td><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-section=\\\"\" + htmlEncode(permission.section) + \"\\\" onclick=\\\"showEditSectionPermissionsModal(this); return false;\\\">\" + htmlEncode(permission.section) + \"</a></td><td>\" +\n        userPermissionsHtml + \"</td><td>\" +\n        groupPermissionsHtml;\n\n    tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnAdminPermissionRowOption\" + id + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-section=\\\"\" + htmlEncode(permission.section) + \"\\\" onclick=\\\"showEditSectionPermissionsModal(this); return false;\\\">Edit Permissions</a></li>\";\n    tableHtmlRows += \"</ul></div></td></tr>\";\n\n    return tableHtmlRows;\n}\n\nfunction showEditSectionPermissionsModal(objMenuItem) {\n    var divEditPermissionsAlert = $(\"#divEditPermissionsAlert\");\n    var divEditPermissionsLoader = $(\"#divEditPermissionsLoader\");\n    var divEditPermissionsViewer = $(\"#divEditPermissionsViewer\");\n\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var section = mnuItem.attr(\"data-section\");\n\n    $(\"#lblEditPermissionsName\").text(section);\n    $(\"#tbodyEditPermissionsUser\").html(\"\");\n    $(\"#tbodyEditPermissionsGroup\").html(\"\");\n\n    divEditPermissionsLoader.show();\n    divEditPermissionsViewer.hide();\n\n    var btnEditPermissionsSave = $(\"#btnEditPermissionsSave\");\n    btnEditPermissionsSave.attr(\"onclick\", \"saveSectionPermissions(this); return false;\");\n    btnEditPermissionsSave.show();\n\n    var modalEditPermissions = $(\"#modalEditPermissions\");\n    modalEditPermissions.modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/permissions/get?token=\" + sessionData.token + \"&section=\" + section + \"&includeUsersAndGroups=true\",\n        success: function (responseJSON) {\n            $(\"#lblEditPermissionsName\").text(responseJSON.response.section);\n\n            //user permissions\n            for (var i = 0; i < responseJSON.response.userPermissions.length; i++) {\n                addEditPermissionUserRow(i, responseJSON.response.userPermissions[i].username, responseJSON.response.userPermissions[i].canView, responseJSON.response.userPermissions[i].canModify, responseJSON.response.userPermissions[i].canDelete);\n            }\n\n            //load users list\n            var userListHtml = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n            for (var i = 0; i < responseJSON.response.users.length; i++) {\n                userListHtml += \"<option>\" + htmlEncode(responseJSON.response.users[i]) + \"</option>\";\n            }\n\n            $(\"#optEditPermissionsUserList\").html(userListHtml);\n\n            //group permissions\n            for (var i = 0; i < responseJSON.response.groupPermissions.length; i++) {\n                addEditPermissionGroupRow(i, responseJSON.response.groupPermissions[i].name, responseJSON.response.groupPermissions[i].canView, responseJSON.response.groupPermissions[i].canModify, responseJSON.response.groupPermissions[i].canDelete);\n            }\n\n            //load groups list\n            var groupListHtml = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n            for (var i = 0; i < responseJSON.response.groups.length; i++) {\n                groupListHtml += \"<option>\" + htmlEncode(responseJSON.response.groups[i]) + \"</option>\";\n            }\n\n            $(\"#optEditPermissionsGroupList\").html(groupListHtml);\n\n            btnEditPermissionsSave.attr(\"data-id\", id);\n            btnEditPermissionsSave.attr(\"data-section\", responseJSON.response.section);\n\n            divEditPermissionsLoader.hide();\n            divEditPermissionsViewer.show();\n        },\n        error: function () {\n            divEditPermissionsLoader.hide();\n        },\n        invalidToken: function () {\n            modalEditPermissions.modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divEditPermissionsAlert,\n        objLoaderPlaceholder: divEditPermissionsLoader\n    });\n}\n\nfunction addEditPermissionUserRow(id, username, canView, canModify, canDelete) {\n    if (id == null)\n        id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRow = \"<tr id=\\\"trEditPermissionsUserRow\" + id + \"\\\"><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(username) + \"<input type=\\\"hidden\\\" value=\\\"\" + htmlEncode(username) + \"\\\"></td>\";\n    tableHtmlRow += \"<td><input type=\\\"checkbox\\\"\" + (canView ? \" checked\" : \"\") + \"></td>\";\n    tableHtmlRow += \"<td><input type=\\\"checkbox\\\"\" + (canModify ? \" checked\" : \"\") + \"></td>\";\n    tableHtmlRow += \"<td><input type=\\\"checkbox\\\"\" + (canDelete ? \" checked\" : \"\") + \"></td>\";\n    tableHtmlRow += \"<td align=\\\"right\\\"><button type=\\\"button\\\" class=\\\"btn btn-warning\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px;\\\" onclick=\\\"$('#trEditPermissionsUserRow\" + id + \"').remove();\\\">Remove</button></td></tr>\";\n\n    $(\"#tbodyEditPermissionsUser\").append(tableHtmlRow);\n}\n\nfunction addEditPermissionGroupRow(id, name, canView, canModify, canDelete) {\n    if (id == null)\n        id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRow = \"<tr id=\\\"trEditPermissionsGroupRow\" + id + \"\\\"><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(name) + \"<input type=\\\"hidden\\\" value=\\\"\" + htmlEncode(name) + \"\\\"></td>\";\n    tableHtmlRow += \"<td><input type=\\\"checkbox\\\"\" + (canView ? \" checked\" : \"\") + \"></td>\";\n    tableHtmlRow += \"<td><input type=\\\"checkbox\\\"\" + (canModify ? \" checked\" : \"\") + \"></td>\";\n    tableHtmlRow += \"<td><input type=\\\"checkbox\\\"\" + (canDelete ? \" checked\" : \"\") + \"></td>\";\n    tableHtmlRow += \"<td align=\\\"right\\\"><button type=\\\"button\\\" class=\\\"btn btn-warning\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px;\\\" onclick=\\\"$('#trEditPermissionsGroupRow\" + id + \"').remove();\\\">Remove</button></td></tr>\";\n\n    $(\"#tbodyEditPermissionsGroup\").append(tableHtmlRow);\n}\n\nfunction saveSectionPermissions(objBtn) {\n    var btn = $(objBtn);\n    var divEditPermissionsAlert = $(\"#divEditPermissionsAlert\");\n\n    var id = btn.attr(\"data-id\");\n    var section = btn.attr(\"data-section\");\n\n    var userPermissions = serializeTableData($(\"#tableEditPermissionsUser\"), 4);\n    var groupPermissions = serializeTableData($(\"#tableEditPermissionsGroup\"), 4);\n\n    var apiUrl = \"api/admin/permissions/set?token=\" + sessionData.token + \"&section=\" + encodeURIComponent(section) + \"&userPermissions=\" + encodeURIComponent(userPermissions) + \"&groupPermissions=\" + encodeURIComponent(groupPermissions) + \"&node=\" + encodeURIComponent(getPrimaryClusterNodeName());\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl,\n        success: function (responseJSON) {\n            var tableHtmlRow = getAdminPermissionsRowHtml(id, responseJSON.response);\n            $(\"#trAdminPermissions\" + id).replaceWith(tableHtmlRow);\n\n            btn.button(\"reset\");\n            $(\"#modalEditPermissions\").modal(\"hide\");\n\n            showAlert(\"success\", \"Permissions Saved!\", \"Section permissions were saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalEditPermissions\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divEditPermissionsAlert\n    });\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/cluster.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n$(function () {\n    $(\"#optInitializeNewClusterQuickIpAddresses\").on(\"change\", function () {\n        var selectedIpAddress = $(\"#optInitializeNewClusterQuickIpAddresses\").val();\n        switch (selectedIpAddress) {\n            case \"blank\":\n                break;\n\n            default:\n                var existingList = $(\"#txtInitializeNewClusterPrimaryNodeIpAddresses\").val();\n\n                if (existingList.indexOf(selectedIpAddress) < 0) {\n                    existingList += selectedIpAddress + \"\\n\";\n                    $(\"#txtInitializeNewClusterPrimaryNodeIpAddresses\").val(existingList);\n                }\n\n                break;\n        }\n    });\n\n    $(\"#optInitializeJoinClusterQuickIpAddresses\").on(\"change\", function () {\n        var selectedIpAddress = $(\"#optInitializeJoinClusterQuickIpAddresses\").val();\n        switch (selectedIpAddress) {\n            case \"blank\":\n                break;\n\n            default:\n                var existingList = $(\"#txtInitializeJoinClusterSecondaryNodeIpAddresses\").val();\n\n                if (existingList.indexOf(selectedIpAddress) < 0) {\n                    existingList += selectedIpAddress + \"\\n\";\n                    $(\"#txtInitializeJoinClusterSecondaryNodeIpAddresses\").val(existingList);\n                }\n\n                break;\n        }\n    });\n\n    $(\"#optEditClusterNodeQuickSelfIpAddresses\").on(\"change\", function () {\n        var selectedIpAddress = $(\"#optEditClusterNodeQuickSelfIpAddresses\").val();\n        switch (selectedIpAddress) {\n            case \"blank\":\n                break;\n\n            default:\n                var existingList = $(\"#txtEditClusterNodeSelfNodeIpAddresses\").val();\n\n                if (existingList.indexOf(selectedIpAddress) < 0) {\n                    existingList += selectedIpAddress + \"\\n\";\n                    $(\"#txtEditClusterNodeSelfNodeIpAddresses\").val(existingList);\n                }\n\n                break;\n        }\n    });\n});\n\nfunction refreshAdminCluster() {\n    var divAdminClusterLoader = $(\"#divAdminClusterLoader\");\n    var divAdminClusterView = $(\"#divAdminClusterView\");\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    divAdminClusterLoader.show();\n    divAdminClusterView.hide();\n\n    HTTPRequest({\n        url: \"api/admin/cluster/state?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            reloadAdminClusterView(responseJSON);\n\n            divAdminClusterLoader.hide();\n            divAdminClusterView.show();\n        },\n        error: function () {\n            divAdminClusterLoader.hide();\n            divAdminClusterView.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divAdminClusterLoader\n    });\n}\n\nfunction updateAdminClusterDataAndGui(responseJSON) {\n    sessionData.info.dnsServerDomain = responseJSON.response.dnsServerDomain;\n    sessionData.info.clusterDomain = responseJSON.response.clusterDomain;\n\n    document.title = responseJSON.response.dnsServerDomain + \" - \" + \"Technitium DNS Server v\" + responseJSON.response.version;\n    $(\"#lblAboutVersion\").text(responseJSON.response.version);\n    $(\"#lblDnsServerDomain\").text(\" - \" + responseJSON.response.dnsServerDomain);\n}\n\nfunction reloadAdminClusterView(responseJSON) {\n    sessionData.info.clusterInitialized = responseJSON.response.clusterInitialized;\n    sessionData.info.clusterNodes = responseJSON.response.clusterNodes;\n    updateAllClusterNodeDropDowns();\n\n    if (responseJSON.response.clusterInitialized) {\n        var selfNodeType;\n\n        for (var i = 0; i < responseJSON.response.clusterNodes.length; i++) {\n            if (responseJSON.response.clusterNodes[i].state == \"Self\") {\n                selfNodeType = responseJSON.response.clusterNodes[i].type;\n                break;\n            }\n        }\n\n        var tableHtmlRows = \"\";\n\n        for (var i = 0; i < responseJSON.response.clusterNodes.length; i++) {\n            var ipAddresses = \"\";\n            var ipAddressesCsv = \"\";\n\n            for (var j = 0; j < responseJSON.response.clusterNodes[i].ipAddresses.length; j++) {\n                ipAddresses += htmlEncode(responseJSON.response.clusterNodes[i].ipAddresses[j]) + \"</br>\";\n\n                if (ipAddressesCsv.length == 0)\n                    ipAddressesCsv = responseJSON.response.clusterNodes[i].ipAddresses[j];\n                else\n                    ipAddressesCsv += \",\" + responseJSON.response.clusterNodes[i].ipAddresses[j];\n            }\n\n            var nodeType;\n\n            switch (responseJSON.response.clusterNodes[i].type) {\n                case \"Primary\":\n                    nodeType = \"<span class=\\\"label label-primary\\\">Primary</span>\";\n                    break;\n\n                case \"Secondary\":\n                    nodeType = \"<span class=\\\"label label-primary\\\">Secondary</span>\";\n                    break;\n\n                default:\n                    nodeType = \"<span class=\\\"label label-warning\\\">Unknown</span>\";\n                    break;\n            }\n\n            var clusterNodestate;\n\n            switch (responseJSON.response.clusterNodes[i].state) {\n                case \"Self\":\n                    clusterNodestate = \"<span class=\\\"label label-default\\\">Self</span>\";\n                    break;\n\n                case \"Connected\":\n                    clusterNodestate = \"<span class=\\\"label label-success\\\">Connected</span>\";\n                    break;\n\n                case \"Unreachable\":\n                    clusterNodestate = \"<span class=\\\"label label-warning\\\">Unreachable</span>\";\n                    break;\n\n                default:\n                    clusterNodestate = \"<span class=\\\"label label-warning\\\">Unknown</span>\";\n                    break;\n            }\n\n            var upSince = \"\";\n\n            if (responseJSON.response.clusterNodes[i].upSince != null)\n                upSince = moment(responseJSON.response.clusterNodes[i].upSince).local().format(\"YYYY-MM-DD HH:mm\") + \"<br /><span style=\\\"font-size: 12px\\\">(\" + moment(responseJSON.response.clusterNodes[i].upSince).fromNow() + \")</span>\";\n\n            var lastSeen = \"\";\n            var lastSynced = \"\";\n\n            switch (responseJSON.response.clusterNodes[i].state) {\n                case \"Self\":\n                    if (responseJSON.response.clusterNodes[i].type == \"Secondary\") {\n                        if (responseJSON.response.clusterNodes[i].configLastSynced != null)\n                            lastSynced = moment(responseJSON.response.clusterNodes[i].configLastSynced).local().format(\"YYYY-MM-DD HH:mm\") + \"<br /><span style=\\\"font-size: 12px\\\">(\" + moment(responseJSON.response.clusterNodes[i].configLastSynced).fromNow() + \")</span>\";\n                    }\n                    break;\n\n                default:\n                    if (responseJSON.response.clusterNodes[i].lastSeen != null)\n                        lastSeen = moment(responseJSON.response.clusterNodes[i].lastSeen).local().format(\"YYYY-MM-DD HH:mm\") + \"<br /><span style=\\\"font-size: 12px\\\">(\" + moment(responseJSON.response.clusterNodes[i].lastSeen).fromNow() + \")</span>\";\n\n                    break;\n            }\n\n            tableHtmlRows += \"<tr id=\\\"trAdminClusterNode\" + i + \"\\\"><td>\" + htmlEncode(responseJSON.response.clusterNodes[i].name) + \"</td><td>\" +\n                ipAddresses + \"</td><td>\" +\n                htmlEncode(responseJSON.response.clusterNodes[i].url) + \"</td><td>\" +\n                nodeType + \"</td><td>\" +\n                clusterNodestate + \"</td><td>\" +\n                upSince + \"</td><td>\" +\n                lastSeen + \"</td><td>\" +\n                lastSynced;\n\n            tableHtmlRows += \"</td>\";\n\n            tableHtmlRows += \"<td align=\\\"right\\\">\";\n\n            switch (selfNodeType) {\n                case \"Primary\":\n                    tableHtmlRows += \"<div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnAdminClusterNodeRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n\n                    if (responseJSON.response.clusterNodes[i].state == \"Self\")\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-node-name=\\\"\" + htmlEncode(responseJSON.response.clusterNodes[i].name) + \"\\\" data-node-ip=\\\"\" + ipAddressesCsv + \"\\\" onclick=\\\"showEditSelfClusterNodeModal(this); return false;\\\">Edit Node</a></li>\";\n\n                    if (responseJSON.response.clusterNodes[i].type == \"Secondary\")\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-node-id=\\\"\" + htmlEncode(responseJSON.response.clusterNodes[i].id) + \"\\\" data-node-name=\\\"\" + htmlEncode(responseJSON.response.clusterNodes[i].name) + \"\\\" onclick=\\\"showRemoveSecondaryClusterNodeModal(this); return false;\\\">Remove Node</a></li>\";\n\n                    tableHtmlRows += \"</ul></div>\";\n                    break;\n\n                case \"Secondary\":\n                    if (responseJSON.response.clusterNodes[i].state == \"Self\") {\n                        tableHtmlRows += \"<div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnAdminClusterNodeRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-node-name=\\\"\" + htmlEncode(responseJSON.response.clusterNodes[i].name) + \"\\\" data-node-ip=\\\"\" + ipAddressesCsv + \"\\\" onclick=\\\"showEditSelfClusterNodeModal(this); return false;\\\">Edit Node</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-node-name=\\\"\" + htmlEncode(responseJSON.response.clusterNodes[i].name) + \"\\\" onclick=\\\"showPromoteToPrimaryClusterNodeModal(this); return false;\\\">Promote To Primary</a></li>\";\n\n                        tableHtmlRows += \"</ul></div>\";\n                    }\n                    else if (responseJSON.response.clusterNodes[i].type == \"Primary\") {\n                        tableHtmlRows += \"<div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnAdminClusterNodeRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-node-name=\\\"\" + htmlEncode(responseJSON.response.clusterNodes[i].name) + \"\\\" data-node-url=\\\"\" + htmlEncode(responseJSON.response.clusterNodes[i].url) + \"\\\" data-node-ip=\\\"\" + ipAddressesCsv + \"\\\" onclick=\\\"showEditPrimaryClusterNodeModal(this); return false;\\\">Edit Node</a></li>\";\n\n                        tableHtmlRows += \"</ul></div>\";\n                    }\n\n                    break;\n            }\n\n            tableHtmlRows += \"</td>\";\n\n            tableHtmlRows += \"</tr>\";\n        }\n\n        $(\"#divAdminClusterInitialize\").hide();\n\n        switch (selfNodeType) {\n            case \"Primary\":\n                $(\"#btnClusterResync\").hide();\n                $(\"#btnClusterOptions\").show();\n                $(\"#btnClusterLeave\").hide();\n                $(\"#btnClusterDelete\").show();\n                break;\n\n            default:\n                $(\"#btnClusterResync\").show();\n                $(\"#btnClusterOptions\").show();\n                $(\"#btnClusterLeave\").show();\n                $(\"#btnClusterDelete\").hide();\n                break;\n        }\n\n        $(\"#tbodyAdminCluster\").html(tableHtmlRows);\n        $(\"#tfootAdminCluster\").html(\"Total Nodes: \" + responseJSON.response.clusterNodes.length);\n    }\n    else {\n        $(\"#divAdminClusterInitialize\").show();\n        $(\"#btnClusterResync\").hide();\n        $(\"#btnClusterOptions\").hide();\n        $(\"#btnClusterLeave\").hide();\n        $(\"#btnClusterDelete\").hide();\n\n        $(\"#tbodyAdminCluster\").html(\"<tr><td colspan=\\\"9\\\" align=\\\"center\\\">Cluster Not Initialized</td></tr>\");\n        $(\"#tfootAdminCluster\").html(\"\");\n    }\n}\n\nfunction showEditSelfClusterNodeModal(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var nodeName = mnuItem.attr(\"data-node-name\");\n    var nodeIp = mnuItem.attr(\"data-node-ip\");\n\n    var divEditClusterNodeAlert = $(\"#divEditClusterNodeAlert\");\n    var divEditClusterNodeLoader = $(\"#divEditClusterNodeLoader\");\n    var divEditClusterNodeView = $(\"#divEditClusterNodeView\");\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    $(\"#lblEditClusterNodeName\").text(nodeName);\n\n    $(\"#txtEditClusterNodeSelfNodeIpAddresses\").val(nodeIp.replace(/,/g, \"\\n\") + \"\\n\");\n\n    $(\"#divEditClusterNodeSelfNode\").show();\n    $(\"#divEditClusterNodePrimaryNode\").hide();\n\n    $(\"#btnEditClusterNodeSave\").attr(\"onclick\", \"updateSelfClusterNode(this); return false;\");\n\n    divEditClusterNodeLoader.show();\n    divEditClusterNodeView.hide();\n\n    $(\"#modalEditClusterNode\").modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/state?token=\" + sessionData.token + \"&includeServerIpAddresses=true\" + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var optionsHtml = \"<option></option>\";\n\n            for (var i = 0; i < responseJSON.response.serverIpAddresses.length; i++)\n                optionsHtml += \"<option>\" + responseJSON.response.serverIpAddresses[i] + \"</option>\";\n\n            $(\"#optEditClusterNodeQuickSelfIpAddresses\").html(optionsHtml);\n\n            divEditClusterNodeLoader.hide();\n            divEditClusterNodeView.show();\n\n            setTimeout(function () {\n                $(\"#optEditClusterNodeSelfNodeIpAddress\").trigger(\"focus\");\n            }, 1000);\n        },\n        error: function () {\n            divEditClusterNodeLoader.hide();\n        },\n        invalidToken: function () {\n            $(\"#modalEditClusterNode\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divEditClusterNodeAlert,\n        objLoaderPlaceholder: divEditClusterNodeLoader\n    });\n}\n\nfunction updateSelfClusterNode(objBtn) {\n    var divEditClusterNodeAlert = $(\"#divEditClusterNodeAlert\");\n\n    var ipAddresses = cleanTextList($(\"#txtEditClusterNodeSelfNodeIpAddresses\").val());\n    if ((ipAddresses.length === 0) || (ipAddresses === \",\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a node IP address.\", divEditClusterNodeAlert);\n        $(\"#txtEditClusterNodeSelfNodeIpAddresses\").trigger(\"focus\");\n        return;\n    }\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/updateIpAddress?token=\" + sessionData.token + \"&ipAddresses=\" + encodeURIComponent(ipAddresses) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalEditClusterNode\").modal(\"hide\");\n\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Node Updated!\", \"Cluster node was updated successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalEditClusterNode\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divEditClusterNodeAlert\n    });\n}\n\nfunction showEditPrimaryClusterNodeModal(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var nodeName = mnuItem.attr(\"data-node-name\");\n    var nodeUrl = mnuItem.attr(\"data-node-url\");\n    var nodeIp = mnuItem.attr(\"data-node-ip\");\n\n    $(\"#lblEditClusterNodeName\").text(nodeName);\n    $(\"#divEditClusterNodeSelfNode\").hide();\n    $(\"#txtEditClusterNodePrimaryNodeUrl\").val(nodeUrl);\n    $(\"#txtEditClusterNodePrimaryNodeIpAddresses\").val(nodeIp.replace(/,/g, \"\\n\") + \"\\n\");\n    $(\"#divEditClusterNodePrimaryNode\").show();\n    $(\"#btnEditClusterNodeSave\").attr(\"onclick\", \"updatePrimaryClusterNode(this); return false;\");\n\n    hideAlert($(\"#divEditClusterNodeAlert\"));\n\n    $(\"#divEditClusterNodeLoader\").hide();\n    $(\"#divEditClusterNodeView\").show();\n\n    $(\"#modalEditClusterNode\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtEditClusterNodePrimaryNodeUrl\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction updatePrimaryClusterNode(objBtn) {\n    var divEditClusterNodeAlert = $(\"#divEditClusterNodeAlert\");\n\n    var primaryNodeUrl = $(\"#txtEditClusterNodePrimaryNodeUrl\").val();\n    if (primaryNodeUrl === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter the Primary node URL.\", divEditClusterNodeAlert);\n        $(\"#txtEditClusterNodePrimaryNodeUrl\").trigger(\"focus\");\n        return;\n    }\n\n    var primaryNodeIpAddresses = cleanTextList($(\"#txtEditClusterNodePrimaryNodeIpAddresses\").val());\n    if (primaryNodeIpAddresses === \",\")\n        primaryNodeIpAddresses = \"\";\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/secondary/updatePrimary?token=\" + sessionData.token + \"&primaryNodeUrl=\" + encodeURIComponent(primaryNodeUrl) + \"&primaryNodeIpAddresses=\" + encodeURIComponent(primaryNodeIpAddresses) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalEditClusterNode\").modal(\"hide\");\n\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Node Updated!\", \"Cluster node was updated successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalEditClusterNode\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divEditClusterNodeAlert\n    });\n}\n\nfunction showRemoveSecondaryClusterNodeModal(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var secondaryNodeId = mnuItem.attr(\"data-node-id\");\n    var nodeName = mnuItem.attr(\"data-node-name\");\n\n    hideAlert($(\"#divRemoveClusterNodeAlert\"));\n    $(\"#lblRemoveClusterNodeName\").text(nodeName);\n    $(\"#chkRemoveClusterNodeForceRemove\").prop(\"checked\", false);\n    $(\"#btnRemoveClusterNode\").attr(\"data-node-id\", secondaryNodeId);\n\n    $(\"#modalRemoveClusterNode\").modal(\"show\");\n}\n\nfunction removeSecondaryClusterNode(objBtn) {\n    var divRemoveClusterNodeAlert = $(\"#divRemoveClusterNodeAlert\");\n    var btn = $(objBtn);\n\n    var secondaryNodeId = btn.attr(\"data-node-id\");\n    var forceRemove = $(\"#chkRemoveClusterNodeForceRemove\").prop(\"checked\");\n\n    var apiUrl;\n\n    if (forceRemove)\n        apiUrl = \"api/admin/cluster/primary/deleteSecondary\";\n    else\n        apiUrl = \"api/admin/cluster/primary/removeSecondary\";\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl + \"?token=\" + sessionData.token + \"&secondaryNodeId=\" + secondaryNodeId + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalRemoveClusterNode\").modal(\"hide\");\n\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Node Removed!\", \"Cluster node was removed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalRemoveClusterNode\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divRemoveClusterNodeAlert\n    });\n}\n\nfunction showPromoteToPrimaryClusterNodeModal(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var nodeName = mnuItem.attr(\"data-node-name\");\n\n    $(\"#lblPromoteToPrimaryClusterNodeName\").text(nodeName);\n    hideAlert($(\"#divPromoteToPrimaryClusterNodeAlert\"));\n    $(\"#chkPromoteToPrimaryClusterNodeForceDeletePrimary\").prop(\"checked\", false);\n    $(\"#modalPromoteToPrimaryClusterNode\").modal(\"show\");\n}\n\nfunction promoteToPrimaryClusterNode(objBtn) {\n    var divPromoteToPrimaryClusterNodeAlert = $(\"#divPromoteToPrimaryClusterNodeAlert\");\n\n    var forceDeletePrimary = $(\"#chkPromoteToPrimaryClusterNodeForceDeletePrimary\").prop(\"checked\");\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/secondary/promote?token=\" + sessionData.token + \"&forceDeletePrimary=\" + forceDeletePrimary + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#modalPromoteToPrimaryClusterNode\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Promoted!\", \"The selected node was successfully promoted to Primary node in the Cluster.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalPromoteToPrimaryClusterNode\").modal(\"hide\");\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divPromoteToPrimaryClusterNodeAlert\n    });\n}\n\nfunction showInitializeClusterModal() {\n    var divInitializeNewClusterAlert = $(\"#divInitializeNewClusterAlert\");\n    var divInitializeNewClusterLoader = $(\"#divInitializeNewClusterLoader\");\n    var divInitializeNewClusterView = $(\"#divInitializeNewClusterView\");\n\n    divInitializeNewClusterLoader.show();\n    divInitializeNewClusterView.hide();\n\n    $(\"#modalInitializeNewCluster\").modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/state?token=\" + sessionData.token + \"&includeServerIpAddresses=true\",\n        success: function (responseJSON) {\n            if (responseJSON.response.clusterInitialized) {\n                showAlert(\"danger\", \"Error!\", \"Cluster is already initialized.\", divInitializeNewClusterAlert);\n                return;\n            }\n\n            $(\"#txtInitializeNewClusterDomain\").val(\"\");\n            $(\"#txtInitializeNewClusterPrimaryNodeIpAddresses\").val(\"\");\n\n            var optionsHtml = \"<option></option>\";\n\n            for (var i = 0; i < responseJSON.response.serverIpAddresses.length; i++)\n                optionsHtml += \"<option>\" + responseJSON.response.serverIpAddresses[i] + \"</option>\";\n\n            $(\"#optInitializeNewClusterQuickIpAddresses\").html(optionsHtml);\n\n            divInitializeNewClusterLoader.hide();\n            divInitializeNewClusterView.show();\n\n            setTimeout(function () {\n                $(\"#txtInitializeNewClusterDomain\").trigger(\"focus\");\n            }, 1000);\n        },\n        invalidToken: function () {\n            $(\"#modalInitializeNewCluster\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divInitializeNewClusterAlert,\n        objLoaderPlaceholder: divInitializeNewClusterLoader\n    });\n}\n\nfunction initializeNewCluster(objBtn) {\n    var divInitializeNewClusterAlert = $(\"#divInitializeNewClusterAlert\");\n\n    var clusterDomain = $(\"#txtInitializeNewClusterDomain\").val();\n    if (clusterDomain === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter the Cluster domain name.\", divInitializeNewClusterAlert);\n        $(\"#txtInitializeNewClusterDomain\").trigger(\"focus\");\n        return;\n    }\n\n    var primaryNodeIpAddresses = cleanTextList($(\"#txtInitializeNewClusterPrimaryNodeIpAddresses\").val());\n    if ((primaryNodeIpAddresses.length === 0) || (primaryNodeIpAddresses === \",\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a Primary node IP address.\", divInitializeNewClusterAlert);\n        $(\"#txtInitializeNewClusterPrimaryNodeIpAddresses\").trigger(\"focus\");\n        return;\n    }\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/init?token=\" + sessionData.token + \"&clusterDomain=\" + encodeURIComponent(clusterDomain) + \"&primaryNodeIpAddresses=\" + encodeURIComponent(primaryNodeIpAddresses),\n        success: function (responseJSON) {\n            $(\"#modalInitializeNewCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            updateAdminClusterDataAndGui(responseJSON);\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Cluster Initialized!\", \"A new cluster was initialized successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalInitializeNewCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divInitializeNewClusterAlert\n    });\n}\n\nfunction showInitializeJoinClusterModal() {\n    var divInitializeJoinClusterAlert = $(\"#divInitializeJoinClusterAlert\");\n    var divInitializeJoinClusterLoader = $(\"#divInitializeJoinClusterLoader\");\n    var divInitializeJoinClusterView = $(\"#divInitializeJoinClusterView\");\n\n    divInitializeJoinClusterAlert.html(\"\");\n    divInitializeJoinClusterLoader.show();\n    divInitializeJoinClusterView.hide();\n\n    $(\"#modalInitializeJoinCluster\").modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/state?token=\" + sessionData.token + \"&includeServerIpAddresses=true\",\n        success: function (responseJSON) {\n            if (responseJSON.response.clusterInitialized) {\n                showAlert(\"danger\", \"Error!\", \"Cluster is already initialized.\", divInitializeJoinClusterAlert);\n                return;\n            }\n\n            $(\"#txtInitializeJoinClusterSecondaryNodeIpAddresses\").val(\"\");\n\n            var optionsHtml = \"<option></option>\";\n\n            for (var i = 0; i < responseJSON.response.serverIpAddresses.length; i++)\n                optionsHtml += \"<option>\" + responseJSON.response.serverIpAddresses[i] + \"</option>\";\n\n            $(\"#optInitializeJoinClusterQuickIpAddresses\").html(optionsHtml);\n\n            $(\"#txtInitializeJoinClusterPrimaryNodeUrl\").val(\"\");\n            $(\"#txtInitializeJoinClusterPrimaryNodeIpAddress\").val(\"\");\n            $(\"#rdInitializeJoinClusterCertificateValidationDefault\").prop(\"checked\", true);\n            $(\"#txtInitializeJoinClusterPrimaryNodeUsername\").val(\"admin\");\n            $(\"#txtInitializeJoinClusterPrimaryNodePassword\").prop(\"disabled\", false);\n            $(\"#txtInitializeJoinClusterPrimaryNodePassword\").val(\"\");\n            $(\"#divInitializeJoinClusterPrimaryNode2faTotp\").hide();\n            $(\"#txtInitializeJoinClusterPrimaryNode2faTotp\").val(\"\");\n\n            divInitializeJoinClusterLoader.hide();\n            divInitializeJoinClusterView.show();\n\n            setTimeout(function () {\n                $(\"#txtInitializeJoinClusterSecondaryNodeIpAddresses\").trigger(\"focus\");\n            }, 1000);\n        },\n        invalidToken: function () {\n            $(\"#modalInitializeJoinCluster\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divInitializeJoinClusterAlert,\n        objLoaderPlaceholder: divInitializeJoinClusterLoader\n    });\n}\n\nfunction initializeJoinCluster(objBtn) {\n    var divInitializeJoinClusterAlert = $(\"#divInitializeJoinClusterAlert\");\n\n    var secondaryNodeIpAddresses = cleanTextList($(\"#txtInitializeJoinClusterSecondaryNodeIpAddresses\").val());\n    if ((secondaryNodeIpAddresses.length === 0) || (secondaryNodeIpAddresses === \",\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please select a Secondary node IP address.\", divInitializeJoinClusterAlert);\n        $(\"#txtInitializeJoinClusterSecondaryNodeIpAddresses\").trigger(\"focus\");\n        return;\n    }\n\n    var primaryNodeUrl = $(\"#txtInitializeJoinClusterPrimaryNodeUrl\").val();\n    if (primaryNodeUrl === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter the Primary node URL.\", divInitializeJoinClusterAlert);\n        $(\"#txtInitializeJoinClusterPrimaryNodeUrl\").trigger(\"focus\");\n        return;\n    }\n\n    var primaryNodeIpAddress = $(\"#txtInitializeJoinClusterPrimaryNodeIpAddress\").val();\n    var ignoreCertificateErrors = $(\"input[name=rdInitializeJoinClusterCertificateValidation]:checked\").val();\n\n    var primaryNodeUsername = $(\"#txtInitializeJoinClusterPrimaryNodeUsername\").val();\n    if (primaryNodeUsername === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter the Primary node admin username.\", divInitializeJoinClusterAlert);\n        $(\"#txtInitializeJoinClusterPrimaryNodeUsername\").trigger(\"focus\");\n        return;\n    }\n\n    var primaryNodePassword = $(\"#txtInitializeJoinClusterPrimaryNodePassword\").val();\n    if (primaryNodePassword === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter the Primary node admin password.\", divInitializeJoinClusterAlert);\n        $(\"#txtInitializeJoinClusterPrimaryNodePassword\").trigger(\"focus\");\n        return;\n    }\n\n    var primaryNodeTotp = $(\"#txtInitializeJoinClusterPrimaryNode2faTotp\").val();\n    if ($(\"#divInitializeJoinClusterPrimaryNode2faTotp\").is(\":visible\")) {\n        if (primaryNodeTotp === \"\") {\n            showAlert(\"warning\", \"Missing!\", \"Please enter the Primary node admin user's OTP.\", divInitializeJoinClusterAlert);\n            $(\"#txtInitializeJoinClusterPrimaryNode2faTotp\").trigger(\"focus\");\n            return;\n        }\n    }\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/initJoin\",\n        method: \"POST\",\n        data: \"token=\" + sessionData.token + \"&secondaryNodeIpAddresses=\" + encodeURIComponent(secondaryNodeIpAddresses)\n            + \"&primaryNodeUrl=\" + encodeURIComponent(primaryNodeUrl) + \"&primaryNodeIpAddress=\" + encodeURIComponent(primaryNodeIpAddress) + \"&ignoreCertificateErrors=\" + ignoreCertificateErrors\n            + \"&primaryNodeUsername=\" + encodeURIComponent(primaryNodeUsername) + \"&primaryNodePassword=\" + encodeURIComponent(primaryNodePassword) + \"&primaryNodeTotp=\" + encodeURIComponent(primaryNodeTotp),\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalInitializeJoinCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            updateAdminClusterDataAndGui(responseJSON);\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Joined Cluster!\", \"Joined the cluster successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalInitializeJoinCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        twoFactorAuthRequired: function () {\n            btn.button(\"reset\");\n\n            $(\"#txtInitializeJoinClusterPrimaryNodePassword\").prop(\"disabled\", true);\n            $(\"#divInitializeJoinClusterPrimaryNode2faTotp\").show();\n            $(\"#txtInitializeJoinClusterPrimaryNode2faTotp\").trigger(\"focus\");\n        },\n        objAlertPlaceholder: divInitializeJoinClusterAlert\n    });\n}\n\nfunction resyncCluster(objBtn) {\n    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?\"))\n        return;\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/secondary/resync?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Resync Triggered!\", \"A full config resync was triggered successfully. Please check the Logs for confirmation.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction showClusterOptionsModal() {\n    var divClusterOptionsAlert = $(\"#divClusterOptionsAlert\");\n    var divClusterOptionsLoader = $(\"#divClusterOptionsLoader\");\n    var divClusterOptionsView = $(\"#divClusterOptionsView\");\n\n    divClusterOptionsLoader.show();\n    divClusterOptionsView.hide();\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    $(\"#modalClusterOptions\").modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/state?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var selfNodeType;\n\n            for (var i = 0; i < responseJSON.response.clusterNodes.length; i++) {\n                if (responseJSON.response.clusterNodes[i].state == \"Self\") {\n                    selfNodeType = responseJSON.response.clusterNodes[i].type;\n                    break;\n                }\n            }\n\n            var isPrimaryNode = selfNodeType == \"Primary\";\n\n            $(\"#txtClusterOptionsHeartbeatRefreshIntervalSeconds\").attr(\"disabled\", !isPrimaryNode);\n            $(\"#txtClusterOptionsHeartbeatRetryIntervalSeconds\").attr(\"disabled\", !isPrimaryNode);\n            $(\"#txtClusterOptionsConfigRefreshIntervalSeconds\").attr(\"disabled\", !isPrimaryNode);\n            $(\"#txtClusterOptionsConfigRetryIntervalSeconds\").attr(\"disabled\", !isPrimaryNode);\n\n            if (isPrimaryNode)\n                $(\"#btnClusterOptionsSave\").show();\n            else\n                $(\"#btnClusterOptionsSave\").hide();\n\n            $(\"#txtClusterOptionsClusterDomain\").val(responseJSON.response.clusterDomain);\n            $(\"#txtClusterOptionsHeartbeatRefreshIntervalSeconds\").val(responseJSON.response.heartbeatRefreshIntervalSeconds);\n            $(\"#txtClusterOptionsHeartbeatRetryIntervalSeconds\").val(responseJSON.response.heartbeatRetryIntervalSeconds);\n            $(\"#txtClusterOptionsConfigRefreshIntervalSeconds\").val(responseJSON.response.configRefreshIntervalSeconds);\n            $(\"#txtClusterOptionsConfigRetryIntervalSeconds\").val(responseJSON.response.configRetryIntervalSeconds);\n\n            divClusterOptionsLoader.hide();\n            divClusterOptionsView.show();\n\n            setTimeout(function () {\n                $(\"#txtClusterOptionsHeartbeatRefreshIntervalSeconds\").trigger(\"focus\");\n            }, 1000);\n        },\n        invalidToken: function () {\n            $(\"#modalClusterOptions\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divClusterOptionsAlert,\n        objLoaderPlaceholder: divClusterOptionsLoader\n    });\n}\n\nfunction saveClusterOptions(objBtn) {\n    var divClusterOptionsAlert = $(\"#divClusterOptionsAlert\");\n\n    var heartbeatRefreshIntervalSeconds = $(\"#txtClusterOptionsHeartbeatRefreshIntervalSeconds\").val();\n    if (heartbeatRefreshIntervalSeconds === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a value for Heartbeat Refresh Interval.\", divClusterOptionsAlert);\n        $(\"#txtClusterOptionsHeartbeatRefreshIntervalSeconds\").trigger(\"focus\");\n        return;\n    }\n\n    var heartbeatRetryIntervalSeconds = $(\"#txtClusterOptionsHeartbeatRetryIntervalSeconds\").val();\n    if (heartbeatRetryIntervalSeconds === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a value for Heartbeat Retry Interval.\", divClusterOptionsAlert);\n        $(\"#txtClusterOptionsHeartbeatRetryIntervalSeconds\").trigger(\"focus\");\n        return;\n    }\n\n    var configRefreshIntervalSeconds = $(\"#txtClusterOptionsConfigRefreshIntervalSeconds\").val();\n    if (configRefreshIntervalSeconds === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a value for Config Refresh Interval.\", divClusterOptionsAlert);\n        $(\"#txtClusterOptionsConfigRefreshIntervalSeconds\").trigger(\"focus\");\n        return;\n    }\n\n    var configRetryIntervalSeconds = $(\"#txtClusterOptionsConfigRetryIntervalSeconds\").val();\n    if (configRetryIntervalSeconds === \"\") {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a value for Config Retry Interval.\", divClusterOptionsAlert);\n        $(\"#txtClusterOptionsConfigRetryIntervalSeconds\").trigger(\"focus\");\n        return;\n    }\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/primary/setOptions?token=\" + sessionData.token\n            + \"&heartbeatRefreshIntervalSeconds=\" + heartbeatRefreshIntervalSeconds + \"&heartbeatRetryIntervalSeconds=\" + heartbeatRetryIntervalSeconds\n            + \"&configRefreshIntervalSeconds=\" + configRefreshIntervalSeconds + \"&configRetryIntervalSeconds=\" + configRetryIntervalSeconds\n            + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#modalClusterOptions\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Options Saved!\", \"The Cluster options were saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalClusterOptions\").modal(\"hide\");\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divClusterOptionsAlert,\n    });\n}\n\nfunction showLeaveClusterModal() {\n    hideAlert($(\"#divLeaveClusterAlert\"));\n    $(\"#chkLeaveClusterForceLeave\").prop(\"checked\", false);\n    $(\"#modalLeaveCluster\").modal(\"show\");\n}\n\nfunction leaveCluster(objBtn) {\n    var divLeaveClusterAlert = $(\"#divLeaveClusterAlert\");\n\n    var forceLeave = $(\"#chkLeaveClusterForceLeave\").prop(\"checked\");\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/secondary/leave?token=\" + sessionData.token + \"&forceLeave=\" + forceLeave + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#modalLeaveCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            updateAdminClusterDataAndGui(responseJSON);\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Left Cluster!\", \"Left the Cluster successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalLeaveCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divLeaveClusterAlert\n    });\n}\n\nfunction showDeleteClusterModal() {\n    hideAlert($(\"#divDeleteClusterAlert\"));\n    $(\"#chkDeleteClusterForceDelete\").prop(\"checked\", false);\n    $(\"#modalDeleteCluster\").modal(\"show\");\n}\n\nfunction deleteCluster(objBtn) {\n    var divDeleteClusterAlert = $(\"#divDeleteClusterAlert\");\n\n    var forceDelete = $(\"#chkDeleteClusterForceDelete\").prop(\"checked\");\n\n    var node = $(\"#optAdminClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/admin/cluster/primary/delete?token=\" + sessionData.token + \"&forceDelete=\" + forceDelete + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#modalDeleteCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            updateAdminClusterDataAndGui(responseJSON);\n            reloadAdminClusterView(responseJSON);\n\n            showAlert(\"success\", \"Cluster Deleted!\", \"Cluster was deleted successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalDeleteCluster\").modal(\"hide\");\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDeleteClusterAlert\n    });\n}\n\nfunction getPrimaryClusterNodeName() {\n    if (sessionData.info.clusterInitialized) {\n        for (var i = 0; i < sessionData.info.clusterNodes.length; i++) {\n            if (sessionData.info.clusterNodes[i].type == \"Primary\")\n                return sessionData.info.clusterNodes[i].name;\n        }\n    }\n\n    return \"\";\n}\n\nfunction updateAllClusterNodeDropDowns() {\n    updateClusterNodeDropDown($(\"#optDashboardClusterNode\"), true);\n    updateClusterNodeDropDown($(\"#optZonesClusterNode\"));\n    updateClusterNodeDropDown($(\"#optEditZoneClusterNode\"));\n    updateClusterNodeDropDown($(\"#optCachedZonesClusterNode\"));\n    updateClusterNodeDropDown($(\"#optDnsClientClusterNode\"));\n    updateClusterNodeDropDown($(\"#optSettingsClusterNode\"), true);\n    updateClusterNodeDropDown($(\"#optDhcpClusterNode\"));\n    updateClusterNodeDropDown($(\"#optAdminSessionsClusterNode\"));\n    updateClusterNodeDropDown($(\"#optAdminClusterNode\"));\n    updateClusterNodeDropDown($(\"#optLogsClusterNode\"));\n}\n\nfunction updateClusterNodeDropDown(optClusterNode, addClusterNode, selectedNode) {\n    if (sessionData.info.clusterInitialized) {\n        if (selectedNode == null)\n            selectedNode = optClusterNode.val();\n\n        var html = \"\";\n\n        if (addClusterNode)\n            html += \"<option value=\\\"cluster\\\">Cluster</option>\";\n\n        for (var i = 0; i < sessionData.info.clusterNodes.length; i++)\n            html += \"<option value=\\\"\" + htmlEncode(sessionData.info.clusterNodes[i].name) + \"\\\">\" + htmlEncode(sessionData.info.clusterNodes[i].name) + \" (\" + htmlEncode(sessionData.info.clusterNodes[i].type.toLowerCase()) + \")\" + \"</option>\";\n\n        optClusterNode.html(html);\n\n        if ((selectedNode == null) || (selectedNode == \"\")) {\n            if (addClusterNode)\n                selectedNode = \"cluster\";\n            else\n                selectedNode = sessionData.info.dnsServerDomain;\n        }\n\n        optClusterNode.val(selectedNode);\n\n        if ((optClusterNode.val() == null) && (sessionData.info.clusterNodes.length > 0))\n            optClusterNode.val(sessionData.info.clusterNodes[0].name);\n\n        optClusterNode.show();\n    }\n    else {\n        optClusterNode.hide();\n        optClusterNode.html(\"<option></option>\");\n        optClusterNode.val(\"\");\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/common.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nfunction htmlEncode(value) {\n    return $('<div/>').text(value).html().replace(/\"/g, \"&quot;\");\n}\n\nfunction htmlDecode(value) {\n    return $('<div/>').html(value).text();\n}\n\nfunction HTTPRequest(url, method, data, isTextResponse, success, error, invalidToken, twoFactorAuthRequired, objAlertPlaceholder, objLoaderPlaceholder, processData, contentType, dontHideAlert, showInnerError) {\n    var finalUrl;\n\n    if ((url != null) && (url.url != null))\n        finalUrl = arguments[0].url;\n    else\n        finalUrl = url;\n\n    if (method == null)\n        method = arguments[0].method;\n\n    if (method == null)\n        method = \"GET\";\n\n    if (data == null) {\n        if (arguments[0].data == null)\n            data = \"\";\n        else\n            data = arguments[0].data;\n    }\n\n    if (isTextResponse == null)\n        isTextResponse = arguments[0].isTextResponse;\n\n    if (isTextResponse == null)\n        isTextResponse = false;\n\n    var dataType = isTextResponse ? null : \"json\";\n\n    if (success == null)\n        success = arguments[0].success;\n\n    var async = success != null;\n\n    if (error == null)\n        error = arguments[0].error;\n\n    if (invalidToken == null)\n        invalidToken = arguments[0].invalidToken;\n\n    if (twoFactorAuthRequired == null)\n        twoFactorAuthRequired = arguments[0].twoFactorAuthRequired;\n\n    if (objAlertPlaceholder == null)\n        objAlertPlaceholder = arguments[0].objAlertPlaceholder;\n\n    if (dontHideAlert == null)\n        dontHideAlert = arguments[0].dontHideAlert;\n\n    if ((dontHideAlert == null) || !dontHideAlert)\n        hideAlert(objAlertPlaceholder);\n\n    if (showInnerError == null)\n        showInnerError = arguments[0].showInnerError;\n\n    if (showInnerError == null)\n        showInnerError = false;\n\n    if (objLoaderPlaceholder == null)\n        objLoaderPlaceholder = arguments[0].objLoaderPlaceholder;\n\n    if (processData == null)\n        processData = arguments[0].processData;\n\n    if (contentType == null)\n        contentType = arguments[0].contentType;\n\n    if (objLoaderPlaceholder != null)\n        objLoaderPlaceholder.html(\"<div style='width: 64px; height: inherit; margin: auto;'><div style='height: inherit; display: table-cell; vertical-align: middle;'><img src='img/loader.gif'/></div></div>\");\n\n    var successFlag = false;\n\n    $.ajax({\n        type: method,\n        url: finalUrl,\n        data: data,\n        dataType: dataType,\n        async: async,\n        cache: false,\n        processData: processData,\n        contentType: contentType,\n        success: function (response, status, jqXHR) {\n            if (objLoaderPlaceholder != null)\n                objLoaderPlaceholder.html(\"\");\n\n            if (isTextResponse) {\n                if (success == null)\n                    successFlag = true;\n                else\n                    success(response);\n            }\n            else {\n                switch (response.status) {\n                    case \"ok\":\n                        if (success == null)\n                            successFlag = true;\n                        else\n                            success(response);\n\n                        break;\n\n                    case \"invalid-token\":\n                        if (invalidToken != null)\n                            invalidToken();\n                        else {\n                            showAlert(\"danger\", \"Error!\", response.errorMessage + (showInnerError && (response.innerErrorMessage != null) ? \" \" + response.innerErrorMessage : \"\"), objAlertPlaceholder);\n\n                            if (error != null)\n                                error();\n                            else\n                                window.location = \"/\";\n                        }\n                        break;\n\n                    case \"2fa-required\":\n                        if (twoFactorAuthRequired != null) {\n                            twoFactorAuthRequired();\n                        }\n                        else {\n                            showAlert(\"danger\", \"Error!\", response.errorMessage + (showInnerError && (response.innerErrorMessage != null) ? \" \" + response.innerErrorMessage : \"\"), objAlertPlaceholder);\n\n                            if (error != null)\n                                error();\n                        }\n\n                        break;\n\n                    case \"error\":\n                        showAlert(\"danger\", \"Error!\", response.errorMessage + (showInnerError && (response.innerErrorMessage != null) ? \" \" + response.innerErrorMessage : \"\"), objAlertPlaceholder);\n\n                        if (error != null)\n                            error();\n\n                        break;\n\n                    default:\n                        showAlert(\"danger\", \"Invalid Response!\", \"Server returned invalid response status: \" + response.status, objAlertPlaceholder);\n\n                        if (error != null)\n                            error();\n\n                        break;\n                }\n            }\n        },\n        error: function (jqXHR, textStatus, errorThrown) {\n            if (objLoaderPlaceholder != null)\n                objLoaderPlaceholder.html(\"\");\n\n            if (error != null)\n                error();\n\n            var msg;\n\n            if ((textStatus === \"error\") && (errorThrown === \"\"))\n                msg = \"Unable to connect to the server. Please try again.\"\n            else\n                msg = textStatus + \" - \" + errorThrown;\n\n            showAlert(\"danger\", \"Error!\", msg, objAlertPlaceholder);\n        }\n    });\n\n    return successFlag;\n}\n\nfunction showAlert(type, title, message, objAlertPlaceholder) {\n    var alertHTML = \"<div class=\\\"alert alert-\" + type + \"\\\">\\\n    <button type=\\\"button\\\" class=\\\"close\\\" data-dismiss=\\\"alert\\\">&times;</button>\\\n    <strong>\" + title + \"</strong>&nbsp;\" + htmlEncode(message) + \"\\\n    </div>\";\n\n    if (objAlertPlaceholder == null)\n        objAlertPlaceholder = $(\".AlertPlaceholder\");\n\n    objAlertPlaceholder.html(alertHTML);\n\n    if (type == \"success\") {\n        setTimeout(function () {\n            hideAlert(objAlertPlaceholder);\n        }, 5000);\n    }\n}\n\nfunction hideAlert(objAlertPlaceholder) {\n    if (objAlertPlaceholder == null)\n        objAlertPlaceholder = $(\".AlertPlaceholder\");\n\n    objAlertPlaceholder.html(\"\");\n}\n\nfunction sortTable(tableId, n) {\n    var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;\n    table = document.getElementById(tableId);\n    switching = true;\n    // Set the sorting direction to ascending:\n    dir = \"asc\";\n    /* Make a loop that will continue until\n    no switching has been done: */\n    while (switching) {\n        // Start by saying: no switching is done:\n        switching = false;\n        rows = table.rows;\n        /* Loop through all table rows */\n        for (i = 0; i < (rows.length - 1); i++) {\n            // Start by saying there should be no switching:\n            shouldSwitch = false;\n            /* Get the two elements you want to compare,\n            one from current row and one from the next: */\n            x = rows[i].getElementsByTagName(\"TD\")[n];\n            y = rows[i + 1].getElementsByTagName(\"TD\")[n];\n            /* Check if the two rows should switch place,\n            based on the direction, asc or desc: */\n            if (dir == \"asc\") {\n                if (x.innerText.toLowerCase() > y.innerText.toLowerCase()) {\n                    // If so, mark as a switch and break the loop:\n                    shouldSwitch = true;\n                    break;\n                }\n            } else if (dir == \"desc\") {\n                if (x.innerText.toLowerCase() < y.innerText.toLowerCase()) {\n                    // If so, mark as a switch and break the loop:\n                    shouldSwitch = true;\n                    break;\n                }\n            }\n        }\n        if (shouldSwitch) {\n            /* If a switch has been marked, make the switch\n            and mark that a switch has been done: */\n            rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);\n            switching = true;\n            // Each time a switch is done, increase this count by 1:\n            switchcount++;\n        } else {\n            /* If no switching has been done AND the direction is \"asc\",\n            set the direction to \"desc\" and run the while loop again. */\n            if (switchcount == 0 && dir == \"asc\") {\n                dir = \"desc\";\n                switching = true;\n            }\n        }\n    }\n}\n\nfunction serializeTableData(table, columns, objAlertPlaceholder) {\n    var data = table.find('input:text, :input[type=\"number\"], input:checkbox, input:hidden, select');\n    var output = \"\";\n\n    for (var i = 0; i < data.length; i += columns) {\n        if (i > 0)\n            output += \"|\";\n\n        for (var j = 0; j < columns; j++) {\n            if (j > 0)\n                output += \"|\";\n\n            var cell = $(data[i + j]);\n\n            var cellValue;\n\n            if (cell.attr(\"type\") == \"checkbox\") {\n                cellValue = cell.prop(\"checked\").toString();\n            }\n            else {\n                cellValue = cell.val();\n\n                var optional = (cell.attr(\"data-optional\") === \"true\");\n\n                if ((cellValue === \"\") && !optional) {\n                    showAlert(\"warning\", \"Missing!\", \"Please enter a valid value in the text field in focus.\", objAlertPlaceholder);\n                    cell.focus();\n                    return false;\n                }\n\n                if (cellValue.includes(\"|\")) {\n                    showAlert(\"warning\", \"Invalid Character!\", \"Please edit the value in the text field in focus to remove '|' character.\", objAlertPlaceholder);\n                    cell.focus();\n                    return false;\n                }\n            }\n\n            output += htmlDecode(cellValue);\n        }\n    }\n\n    return output;\n}\n\nfunction cleanTextList(text) {\n    text = text.replace(/\\n/g, \",\");\n\n    while (text.indexOf(\",,\") !== -1) {\n        text = text.replace(/,,/g, \",\");\n    }\n\n    if (text.startsWith(\",\"))\n        text = text.substr(1);\n\n    if (text.endsWith(\",\"))\n        text = text.substr(0, text.length - 1);\n\n    return text;\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/dhcp.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n$(function () {\n    $(\"#chkDhcpScopeDnsUpdates\").on(\"click\", function () {\n        var checked = $(\"#chkDhcpScopeDnsUpdates\").prop(\"checked\");\n\n        $(\"#chkDnsOverwriteForDynamicLease\").prop(\"disabled\", !checked);\n    });\n});\n\nfunction refreshDhcpTab() {\n    if ($(\"#dhcpTabListLeases\").hasClass(\"active\"))\n        refreshDhcpLeases();\n    else if ($(\"#dhcpTabListScopes\").hasClass(\"active\"))\n        refreshDhcpScopes(true);\n    else\n        refreshDhcpLeases();\n}\n\nfunction refreshDhcpLeases() {\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var divDhcpLeasesLoader = $(\"#divDhcpLeasesLoader\");\n    var divDhcpLeases = $(\"#divDhcpLeases\");\n\n    divDhcpLeases.hide();\n    divDhcpLeasesLoader.show();\n\n    HTTPRequest({\n        url: \"api/dhcp/leases/list?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var dhcpLeases = responseJSON.response.leases;\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < dhcpLeases.length; i++) {\n                tableHtmlRows += \"<tr id=\\\"trDhcpLeaseRow\" + i + \"\\\"><td>\" + htmlEncode(dhcpLeases[i].scope) + \"</td><td>\" +\n                    dhcpLeases[i].hardwareAddress + \"</td><td>\" +\n                    dhcpLeases[i].address + \"</td><td><span id=\\\"spanDhcpLeaseType\" + i + \"\\\" class=\\\"label label-\" +\n                    (dhcpLeases[i].type === \"Reserved\" ? \"default\" : \"primary\") + \"\\\">\" + dhcpLeases[i].type + \"</span></td><td>\" +\n                    htmlEncode(dhcpLeases[i].hostName) + \"</td><td>\" +\n                    moment(dhcpLeases[i].leaseObtained).local().format(\"YYYY-MM-DD HH:mm\") + \"</td><td>\" +\n                    moment(dhcpLeases[i].leaseExpires).local().format(\"YYYY-MM-DD HH:mm\");\n\n                tableHtmlRows += \"</td><td><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDhcpLeaseRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                tableHtmlRows += \"<li id=\\\"btnDhcpLeaseReserve\" + i + \"\\\" style=\\\"\" + (dhcpLeases[i].type === \"Dynamic\" ? \"\" : \"display: none;\") + \"\\\"><a href=\\\"#\\\" onclick=\\\"convertToReservedLease(\" + i + \", '\" + dhcpLeases[i].scope + \"', '\" + dhcpLeases[i].clientIdentifier + \"'); return false;\\\">Convert To Reserved Lease</a></li>\";\n                tableHtmlRows += \"<li id=\\\"btnDhcpLeaseUnreserve\" + i + \"\\\" style=\\\"\" + (dhcpLeases[i].type === \"Dynamic\" ? \"display: none;\" : \"\") + \"\\\"><a href=\\\"#\\\" onclick=\\\"convertToDynamicLease(\" + i + \", '\" + dhcpLeases[i].scope + \"', '\" + dhcpLeases[i].clientIdentifier + \"'); return false;\\\">Convert To Dynamic Lease</a></li>\";\n                tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"showRemoveLeaseModal(\" + i + \", '\" + dhcpLeases[i].scope + \"', '\" + dhcpLeases[i].clientIdentifier + \"'); return false;\\\">Remove Lease</a></li>\";\n                tableHtmlRows += \"</ul></div></td></tr>\";\n            }\n\n            $(\"#tableDhcpLeasesBody\").html(tableHtmlRows);\n\n            if (dhcpLeases.length > 0)\n                $(\"#tableDhcpLeasesFooter\").html(\"<tr><td colspan=\\\"8\\\"><b>Total Leases: \" + dhcpLeases.length + \"</b></td></tr>\");\n            else\n                $(\"#tableDhcpLeasesFooter\").html(\"<tr><td colspan=\\\"8\\\" align=\\\"center\\\">No Lease Found</td></tr>\");\n\n            divDhcpLeasesLoader.hide();\n            divDhcpLeases.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDhcpLeasesLoader\n    });\n}\n\nfunction convertToReservedLease(id, scopeName, clientIdentifier) {\n    if (!confirm(\"Are you sure you want to convert the dynamic lease to reserved lease?\"))\n        return;\n\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var btn = $(\"#btnDhcpLeaseRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/dhcp/leases/convertToReserved?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(scopeName) + \"&clientIdentifier=\" + encodeURIComponent(clientIdentifier) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n\n            $(\"#btnDhcpLeaseReserve\" + id).hide();\n            $(\"#btnDhcpLeaseUnreserve\" + id).show();\n\n            var spanDhcpLeaseType = $(\"#spanDhcpLeaseType\" + id);\n            spanDhcpLeaseType.html(\"Reserved\");\n            spanDhcpLeaseType.attr(\"class\", \"label label-default\");\n\n            showAlert(\"success\", \"Reserved!\", \"The dynamic lease was converted to reserved lease successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction convertToDynamicLease(id, scopeName, clientIdentifier) {\n    if (!confirm(\"Are you sure you want to convert the reserved lease to dynamic lease?\"))\n        return;\n\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var btn = $(\"#btnDhcpLeaseRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/dhcp/leases/convertToDynamic?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(scopeName) + \"&clientIdentifier=\" + encodeURIComponent(clientIdentifier) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n\n            $(\"#btnDhcpLeaseReserve\" + id).show();\n            $(\"#btnDhcpLeaseUnreserve\" + id).hide();\n\n            var spanDhcpLeaseType = $(\"#spanDhcpLeaseType\" + id);\n            spanDhcpLeaseType.html(\"Dynamic\");\n            spanDhcpLeaseType.attr(\"class\", \"label label-primary\");\n\n            showAlert(\"success\", \"Unreserved!\", \"The reserved lease was converted to dynamic lease successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction showRemoveLeaseModal(index, scopeName, clientIdentifier) {\n    $(\"#divDhcpRemoveLeaseAlert\").html(\"\");\n    $(\"#btnRemoveDhcpLease\").attr(\"onclick\", \"removeLease(this, \" + index + \", '\" + scopeName + \"', '\" + clientIdentifier + \"');\");\n    $(\"#modalDhcpRemoveLease\").modal(\"show\");\n}\n\nfunction removeLease(objBtn, index, scopeName, clientIdentifier) {\n    var divDhcpRemoveLeaseAlert = $(\"#divDhcpRemoveLeaseAlert\");\n\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/dhcp/leases/remove?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(scopeName) + \"&clientIdentifier=\" + encodeURIComponent(clientIdentifier) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalDhcpRemoveLease\").modal(\"hide\");\n\n            $(\"#trDhcpLeaseRow\" + index).remove();\n\n            var dhcpLeasesLength = $('#tableDhcpLeasesBody >tr').length;\n            if (dhcpLeasesLength > 0)\n                $(\"#tableDhcpLeasesFooter\").html(\"<tr><td colspan=\\\"8\\\"><b>Total Leases: \" + dhcpLeasesLength + \"</b></td></tr>\");\n            else\n                $(\"#tableDhcpLeasesFooter\").html(\"<tr><td colspan=\\\"8\\\" align=\\\"center\\\">No Lease Found</td></tr>\");\n\n            showAlert(\"success\", \"Lease Removed!\", \"The DHCP lease was removed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDhcpRemoveLeaseAlert\n    });\n}\n\nfunction refreshDhcpScopes(checkDisplay) {\n    if (checkDisplay == null)\n        checkDisplay = false;\n\n    var divDhcpEditScope = $(\"#divDhcpEditScope\");\n\n    if (checkDisplay && (divDhcpEditScope.css(\"display\") != \"none\"))\n        return;\n\n    var node = $(\"#optDhcpClusterNode\").val();\n    $(\"#optDhcpClusterNode\").prop(\"disabled\", false);\n\n    var divDhcpViewScopes = $(\"#divDhcpViewScopes\");\n    var divDhcpViewScopesLoader = $(\"#divDhcpViewScopesLoader\");\n\n    divDhcpViewScopes.hide();\n    divDhcpEditScope.hide();\n    divDhcpViewScopesLoader.show();\n\n    HTTPRequest({\n        url: \"api/dhcp/scopes/list?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var dhcpScopes = responseJSON.response.scopes;\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < dhcpScopes.length; i++) {\n                tableHtmlRows += \"<tr id=\\\"trDhcpScopeRow\" + i + \"\\\"><td>\" + htmlEncode(dhcpScopes[i].name) + \"</td><td>\" + dhcpScopes[i].startingAddress + \" - \" + dhcpScopes[i].endingAddress + \"<br />\" + dhcpScopes[i].subnetMask + \"</td><td>\" + dhcpScopes[i].networkAddress + \"<br />\" + dhcpScopes[i].broadcastAddress + \"</td><td>\" + (dhcpScopes[i].interfaceAddress == null ? \"\" : dhcpScopes[i].interfaceAddress) + \"</td>\";\n                tableHtmlRows += \"<td align=\\\"right\\\"><button type=\\\"button\\\" class=\\\"btn btn-primary\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 6px 0;\\\" onclick=\\\"showEditDhcpScope('\" + dhcpScopes[i].name + \"');\\\">Edit</button>\";\n\n                if (dhcpScopes[i].enabled)\n                    tableHtmlRows += \"<button type=\\\"button\\\" class=\\\"btn btn-warning\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 6px 0;\\\" onclick=\\\"disableDhcpScope('\" + dhcpScopes[i].name + \"');\\\">Disable</button>\";\n                else\n                    tableHtmlRows += \"<button type=\\\"button\\\" class=\\\"btn btn-default\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 6px 0;\\\" onclick=\\\"enableDhcpScope('\" + dhcpScopes[i].name + \"');\\\">Enable</button>\";\n\n                tableHtmlRows += \"<button type=\\\"button\\\" class=\\\"btn btn-danger\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 6px 0;\\\" onclick=\\\"deleteDhcpScope(\" + i + \", '\" + dhcpScopes[i].name + \"');\\\">Delete</button></td></tr>\";\n            }\n\n            $(\"#tableDhcpScopesBody\").html(tableHtmlRows);\n\n            if (dhcpScopes.length > 0)\n                $(\"#tableDhcpScopesFooter\").html(\"<tr><td colspan=\\\"5\\\"><b>Total Scopes: \" + dhcpScopes.length + \"</b></td></tr>\");\n            else\n                $(\"#tableDhcpScopesFooter\").html(\"<tr><td colspan=\\\"5\\\" align=\\\"center\\\">No Scope Found</td></tr>\");\n\n            divDhcpViewScopesLoader.hide();\n            divDhcpViewScopes.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDhcpViewScopesLoader\n    });\n}\n\nfunction addDhcpScopeStaticRouteRow(destination, subnetMask, router) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableDhcpScopeStaticRoutesRow\" + id + \"\\\"><td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(destination) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(subnetMask) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(router) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableDhcpScopeStaticRoutesRow\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableDhcpScopeStaticRoutes\").append(tableHtmlRows);\n}\n\nfunction addDhcpScopeVendorInfoRow(identifier, information) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableDhcpScopeVendorInfoRow\" + id + \"\\\"><td><input type=\\\"text\\\" class=\\\"form-control\\\" value='\" + htmlEncode(identifier) + \"' data-optional=\\\"true\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value='\" + htmlEncode(information) + \"'></td>\";\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableDhcpScopeVendorInfoRow\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableDhcpScopeVendorInfo\").append(tableHtmlRows);\n}\n\nfunction addDhcpScopeGenericOptionsRow(optionCode, hexValue) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableDhcpScopeGenericOptionsRow\" + id + \"\\\"><td><input type=\\\"number\\\" min=\\\"0\\\" max=\\\"255\\\" class=\\\"form-control\\\" value='\" + htmlEncode(optionCode) + \"'></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value='\" + htmlEncode(hexValue) + \"'></td>\";\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableDhcpScopeGenericOptionsRow\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableDhcpScopeGenericOptions\").append(tableHtmlRows);\n}\n\nfunction addDhcpScopeExclusionRow(startingAddress, endingAddress) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableDhcpScopeExclusionRow\" + id + \"\\\"><td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(startingAddress) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(endingAddress) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableDhcpScopeExclusionRow\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableDhcpScopeExclusions\").append(tableHtmlRows);\n}\n\nfunction addDhcpScopeReservedLeaseRow(hostName, hardwareAddress, address, comments) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableDhcpScopeReservedLeaseRow\" + id + \"\\\">\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + (hostName == null ? \"\" : htmlEncode(hostName)) + \"\\\" data-optional=\\\"true\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(hardwareAddress) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(address) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + (comments == null ? \"\" : htmlEncode(comments)) + \"\\\" data-optional=\\\"true\\\"></td>\";\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableDhcpScopeReservedLeaseRow\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableDhcpScopeReservedLeases\").append(tableHtmlRows);\n}\n\nfunction clearDhcpScopeForm() {\n    $(\"#txtDhcpScopeName\").attr(\"data-name\", \"\");\n    $(\"#txtDhcpScopeName\").val(\"\");\n    $(\"#txtDhcpScopeStartingAddress\").val(\"\");\n    $(\"#txtDhcpScopeEndingAddress\").val(\"\");\n    $(\"#txtDhcpScopeSubnetMask\").val(\"\");\n    $(\"#txtDhcpScopeLeaseTimeDays\").val(\"1\");\n    $(\"#txtDhcpScopeLeaseTimeHours\").val(\"0\");\n    $(\"#txtDhcpScopeLeaseTimeMinutes\").val(\"0\");\n    $(\"#txtDhcpScopeOfferDelayTime\").val(\"0\");\n    $(\"#chkDhcpScopePingCheckEnabled\").prop(\"checked\", false);\n    $(\"#txtDhcpScopePingCheckTimeout\").val(\"1000\");\n    $(\"#txtDhcpScopePingCheckRetries\").val(\"2\");\n    $(\"#txtDhcpScopeDomainName\").val(\"\");\n    $(\"#txtDhcpScopeDomainSearchStrings\").val(\"\");\n    $(\"#chkDhcpScopeDnsUpdates\").prop(\"checked\", true);\n    $(\"#chkDnsOverwriteForDynamicLease\").prop(\"disabled\", false);\n    $(\"#chkDnsOverwriteForDynamicLease\").prop(\"checked\", false);\n    $(\"#txtDhcpScopeDnsTtl\").val(\"900\");\n    $(\"#txtDhcpScopeServerAddress\").val(\"\");\n    $(\"#txtDhcpScopeServerHostName\").val(\"\");\n    $(\"#txtDhcpScopeBootFileName\").val(\"\");\n    $(\"#txtDhcpScopeRouterAddress\").val(\"\");\n    $(\"#chkUseThisDnsServer\").prop(\"checked\", false);\n    $('#txtDhcpScopeDnsServers').prop(\"disabled\", false);\n    $(\"#txtDhcpScopeDnsServers\").val(\"\");\n    $(\"#txtDhcpScopeWinsServers\").val(\"\");\n    $(\"#txtDhcpScopeNtpServers\").val(\"\");\n    $(\"#txtDhcpScopeNtpServerDomainNames\").val(\"\");\n    $(\"#tableDhcpScopeStaticRoutes\").html(\"\");\n    $(\"#tableDhcpScopeVendorInfo\").html(\"\");\n    $(\"#txtDhcpScopeCAPWAPApIpAddresses\").val(\"\");\n    $(\"#txtDhcpScopeTftpServerAddresses\").val(\"\");\n    $(\"#tableDhcpScopeGenericOptions\").html(\"\");\n    $(\"#tableDhcpScopeExclusions\").html(\"\");\n    $(\"#tableDhcpScopeReservedLeases\").html(\"\");\n    $(\"#chkAllowOnlyReservedLeases\").prop(\"checked\", false);\n    $(\"#chkBlockLocallyAdministeredMacAddresses\").prop(\"checked\", false);\n    $(\"#chkIgnoreClientIdentifierOption\").prop(\"checked\", true);\n    $(\"#btnSaveDhcpScope\").button(\"reset\");\n}\n\nfunction showAddDhcpScope() {\n    clearDhcpScopeForm();\n\n    $(\"#titleDhcpEditScope\").html(\"Add Scope\");\n    $(\"#chkUseThisDnsServer\").prop(\"checked\", true);\n    $('#txtDhcpScopeDnsServers').prop(\"disabled\", true);\n    $(\"#divDhcpViewScopes\").hide();\n    $(\"#divDhcpViewScopesLoader\").hide();\n    $(\"#divDhcpEditScope\").show();\n}\n\nfunction showEditDhcpScope(scopeName) {\n    clearDhcpScopeForm();\n\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    $(\"#titleDhcpEditScope\").html(\"Edit Scope\");\n    var divDhcpViewScopesLoader = $(\"#divDhcpViewScopesLoader\");\n    var divDhcpViewScopes = $(\"#divDhcpViewScopes\");\n    var divDhcpEditScope = $(\"#divDhcpEditScope\");\n\n    divDhcpViewScopes.hide();\n    divDhcpEditScope.hide();\n    divDhcpViewScopesLoader.show();\n\n    HTTPRequest({\n        url: \"api/dhcp/scopes/get?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(scopeName) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#txtDhcpScopeName\").attr(\"data-name\", responseJSON.response.name);\n            $(\"#txtDhcpScopeName\").val(responseJSON.response.name);\n            $(\"#txtDhcpScopeStartingAddress\").val(responseJSON.response.startingAddress);\n            $(\"#txtDhcpScopeEndingAddress\").val(responseJSON.response.endingAddress);\n            $(\"#txtDhcpScopeSubnetMask\").val(responseJSON.response.subnetMask);\n            $(\"#txtDhcpScopeLeaseTimeDays\").val(responseJSON.response.leaseTimeDays);\n            $(\"#txtDhcpScopeLeaseTimeHours\").val(responseJSON.response.leaseTimeHours);\n            $(\"#txtDhcpScopeLeaseTimeMinutes\").val(responseJSON.response.leaseTimeMinutes);\n            $(\"#txtDhcpScopeOfferDelayTime\").val(responseJSON.response.offerDelayTime);\n\n            $(\"#chkDhcpScopePingCheckEnabled\").prop(\"checked\", responseJSON.response.pingCheckEnabled);\n            $(\"#txtDhcpScopePingCheckTimeout\").val(responseJSON.response.pingCheckTimeout);\n            $(\"#txtDhcpScopePingCheckRetries\").val(responseJSON.response.pingCheckRetries);\n\n            if (responseJSON.response.domainName != null)\n                $(\"#txtDhcpScopeDomainName\").val(responseJSON.response.domainName);\n\n            if (responseJSON.response.domainSearchList != null)\n                $(\"#txtDhcpScopeDomainSearchStrings\").val(responseJSON.response.domainSearchList.join(\"\\n\"));\n\n            $(\"#chkDhcpScopeDnsUpdates\").prop(\"checked\", responseJSON.response.dnsUpdates);\n            $(\"#chkDnsOverwriteForDynamicLease\").prop(\"disabled\", !responseJSON.response.dnsUpdates);\n            $(\"#chkDnsOverwriteForDynamicLease\").prop(\"checked\", responseJSON.response.dnsOverwriteForDynamicLease);\n            $(\"#txtDhcpScopeDnsTtl\").val(responseJSON.response.dnsTtl);\n\n            if (responseJSON.response.serverAddress != null)\n                $(\"#txtDhcpScopeServerAddress\").val(responseJSON.response.serverAddress);\n\n            if (responseJSON.response.serverHostName != null)\n                $(\"#txtDhcpScopeServerHostName\").val(responseJSON.response.serverHostName);\n\n            if (responseJSON.response.bootFileName != null)\n                $(\"#txtDhcpScopeBootFileName\").val(responseJSON.response.bootFileName);\n\n            if (responseJSON.response.routerAddress != null)\n                $(\"#txtDhcpScopeRouterAddress\").val(responseJSON.response.routerAddress);\n\n            $(\"#chkUseThisDnsServer\").prop(\"checked\", responseJSON.response.useThisDnsServer);\n            $('#txtDhcpScopeDnsServers').prop(\"disabled\", responseJSON.response.useThisDnsServer);\n\n            if (responseJSON.response.dnsServers != null)\n                $(\"#txtDhcpScopeDnsServers\").val(responseJSON.response.dnsServers.join(\"\\n\"));\n\n            if (responseJSON.response.winsServers != null)\n                $(\"#txtDhcpScopeWinsServers\").val(responseJSON.response.winsServers.join(\"\\n\"));\n\n            if (responseJSON.response.ntpServers != null)\n                $(\"#txtDhcpScopeNtpServers\").val(responseJSON.response.ntpServers.join(\"\\n\"));\n\n            if (responseJSON.response.ntpServerDomainNames != null)\n                $(\"#txtDhcpScopeNtpServerDomainNames\").val(responseJSON.response.ntpServerDomainNames.join(\"\\n\"));\n\n            if (responseJSON.response.staticRoutes != null) {\n                for (var i = 0; i < responseJSON.response.staticRoutes.length; i++) {\n                    addDhcpScopeStaticRouteRow(responseJSON.response.staticRoutes[i].destination, responseJSON.response.staticRoutes[i].subnetMask, responseJSON.response.staticRoutes[i].router);\n                }\n            }\n\n            if (responseJSON.response.vendorInfo != null) {\n                for (var i = 0; i < responseJSON.response.vendorInfo.length; i++) {\n                    addDhcpScopeVendorInfoRow(responseJSON.response.vendorInfo[i].identifier, responseJSON.response.vendorInfo[i].information);\n                }\n            }\n\n            if (responseJSON.response.capwapAcIpAddresses != null)\n                $(\"#txtDhcpScopeCAPWAPApIpAddresses\").val(responseJSON.response.capwapAcIpAddresses.join(\"\\n\"));\n\n            if (responseJSON.response.tftpServerAddresses != null)\n                $(\"#txtDhcpScopeTftpServerAddresses\").val(responseJSON.response.tftpServerAddresses.join(\"\\n\"));\n\n            if (responseJSON.response.genericOptions != null) {\n                for (var i = 0; i < responseJSON.response.genericOptions.length; i++) {\n                    addDhcpScopeGenericOptionsRow(responseJSON.response.genericOptions[i].code, responseJSON.response.genericOptions[i].value);\n                }\n            }\n\n            if (responseJSON.response.exclusions != null) {\n                for (var i = 0; i < responseJSON.response.exclusions.length; i++) {\n                    addDhcpScopeExclusionRow(responseJSON.response.exclusions[i].startingAddress, responseJSON.response.exclusions[i].endingAddress);\n                }\n            }\n\n            if (responseJSON.response.reservedLeases != null) {\n                for (var i = 0; i < responseJSON.response.reservedLeases.length; i++) {\n                    addDhcpScopeReservedLeaseRow(responseJSON.response.reservedLeases[i].hostName, responseJSON.response.reservedLeases[i].hardwareAddress, responseJSON.response.reservedLeases[i].address, responseJSON.response.reservedLeases[i].comments);\n                }\n            }\n\n            $(\"#chkAllowOnlyReservedLeases\").prop(\"checked\", responseJSON.response.allowOnlyReservedLeases);\n            $(\"#chkBlockLocallyAdministeredMacAddresses\").prop(\"checked\", responseJSON.response.blockLocallyAdministeredMacAddresses);\n            $(\"#chkIgnoreClientIdentifierOption\").prop(\"checked\", responseJSON.response.ignoreClientIdentifierOption);\n\n            $(\"#optDhcpClusterNode\").prop(\"disabled\", true);\n\n            divDhcpViewScopesLoader.hide();\n            divDhcpEditScope.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDhcpViewScopesLoader\n    });\n}\n\nfunction saveDhcpScope() {\n    var oldName = $(\"#txtDhcpScopeName\").attr(\"data-name\");\n    var name = $(\"#txtDhcpScopeName\").val();\n    var newName = null;\n\n    if ((oldName !== \"\") && (oldName != name)) {\n        newName = name;\n        name = oldName;\n    }\n\n    var startingAddress = $(\"#txtDhcpScopeStartingAddress\").val();\n    var endingAddress = $(\"#txtDhcpScopeEndingAddress\").val();\n    var subnetMask = $(\"#txtDhcpScopeSubnetMask\").val();\n\n    var leaseTimeDays = $(\"#txtDhcpScopeLeaseTimeDays\").val();\n    var leaseTimeHours = $(\"#txtDhcpScopeLeaseTimeHours\").val();\n    var leaseTimeMinutes = $(\"#txtDhcpScopeLeaseTimeMinutes\").val();\n    var offerDelayTime = $(\"#txtDhcpScopeOfferDelayTime\").val();\n\n    var pingCheckEnabled = $(\"#chkDhcpScopePingCheckEnabled\").prop(\"checked\");\n    var pingCheckTimeout = $(\"#txtDhcpScopePingCheckTimeout\").val();\n    var pingCheckRetries = $(\"#txtDhcpScopePingCheckRetries\").val();\n\n    var domainName = $(\"#txtDhcpScopeDomainName\").val();\n    var domainSearchList = cleanTextList($(\"#txtDhcpScopeDomainSearchStrings\").val());\n    var dnsUpdates = $(\"#chkDhcpScopeDnsUpdates\").prop(\"checked\");\n    var dnsOverwriteForDynamicLease = $(\"#chkDnsOverwriteForDynamicLease\").prop(\"checked\");\n    var dnsTtl = $(\"#txtDhcpScopeDnsTtl\").val();\n\n    var serverAddress = $(\"#txtDhcpScopeServerAddress\").val();\n    var serverHostName = $(\"#txtDhcpScopeServerHostName\").val();\n    var bootFileName = $(\"#txtDhcpScopeBootFileName\").val();\n    var routerAddress = $(\"#txtDhcpScopeRouterAddress\").val();\n\n    var useThisDnsServer = $(\"#chkUseThisDnsServer\").prop('checked');\n    var dnsServers = cleanTextList($(\"#txtDhcpScopeDnsServers\").val());\n    var winsServers = cleanTextList($(\"#txtDhcpScopeWinsServers\").val());\n    var ntpServers = cleanTextList($(\"#txtDhcpScopeNtpServers\").val());\n    var ntpServerDomainNames = cleanTextList($(\"#txtDhcpScopeNtpServerDomainNames\").val());\n\n    var staticRoutes = serializeTableData($(\"#tableDhcpScopeStaticRoutes\"), 3);\n    if (staticRoutes === false)\n        return;\n\n    var vendorInfo = serializeTableData($(\"#tableDhcpScopeVendorInfo\"), 2);\n    if (vendorInfo === false)\n        return;\n\n    var capwapAcIpAddresses = cleanTextList($(\"#txtDhcpScopeCAPWAPApIpAddresses\").val());\n\n    var tftpServerAddresses = cleanTextList($(\"#txtDhcpScopeTftpServerAddresses\").val());\n\n    var genericOptions = serializeTableData($(\"#tableDhcpScopeGenericOptions\"), 2);\n    if (genericOptions === false)\n        return;\n\n    var exclusions = serializeTableData($(\"#tableDhcpScopeExclusions\"), 2);\n    if (exclusions === false)\n        return;\n\n    var reservedLeases = serializeTableData($(\"#tableDhcpScopeReservedLeases\"), 4);\n    if (reservedLeases === false)\n        return;\n\n    var allowOnlyReservedLeases = $(\"#chkAllowOnlyReservedLeases\").prop('checked');\n    var blockLocallyAdministeredMacAddresses = $(\"#chkBlockLocallyAdministeredMacAddresses\").prop('checked');\n    var ignoreClientIdentifierOption = $(\"#chkIgnoreClientIdentifierOption\").prop('checked');\n\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var btn = $(\"#btnSaveDhcpScope\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/dhcp/scopes/set?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        method: \"POST\",\n        data: \"name=\" + encodeURIComponent(name) + (newName == null ? \"\" : \"&newName=\" + encodeURIComponent(newName)) + \"&startingAddress=\" + encodeURIComponent(startingAddress) + \"&endingAddress=\" + encodeURIComponent(endingAddress) + \"&subnetMask=\" + encodeURIComponent(subnetMask) +\n            \"&leaseTimeDays=\" + leaseTimeDays + \"&leaseTimeHours=\" + leaseTimeHours + \"&leaseTimeMinutes=\" + leaseTimeMinutes + \"&offerDelayTime=\" + offerDelayTime + \"&pingCheckEnabled=\" + pingCheckEnabled + \"&pingCheckTimeout=\" + pingCheckTimeout + \"&pingCheckRetries=\" + pingCheckRetries +\n            \"&domainName=\" + encodeURIComponent(domainName) + \"&domainSearchList=\" + encodeURIComponent(domainSearchList) + \"&dnsUpdates=\" + dnsUpdates + \"&dnsOverwriteForDynamicLease=\" + dnsOverwriteForDynamicLease + \"&dnsTtl=\" + dnsTtl + \"&serverAddress=\" + encodeURIComponent(serverAddress) + \"&serverHostName=\" + encodeURIComponent(serverHostName) + \"&bootFileName=\" + encodeURIComponent(bootFileName) +\n            \"&routerAddress=\" + encodeURIComponent(routerAddress) + \"&useThisDnsServer=\" + useThisDnsServer + (useThisDnsServer ? \"\" : \"&dnsServers=\" + encodeURIComponent(dnsServers)) + \"&winsServers=\" + encodeURIComponent(winsServers) + \"&ntpServers=\" + encodeURIComponent(ntpServers) + \"&ntpServerDomainNames=\" + encodeURIComponent(ntpServerDomainNames) +\n            \"&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,\n        processData: false,\n        success: function (responseJSON) {\n            refreshDhcpScopes();\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Scope Saved!\", \"DHCP Scope was saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction disableDhcpScope(scopeName) {\n    if (!confirm(\"Are you sure you want to disable the DHCP scope '\" + scopeName + \"'?\"))\n        return;\n\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var divDhcpViewScopesLoader = $(\"#divDhcpViewScopesLoader\");\n    var divDhcpViewScopes = $(\"#divDhcpViewScopes\");\n    var divDhcpEditScope = $(\"#divDhcpEditScope\");\n\n    divDhcpViewScopes.hide();\n    divDhcpEditScope.hide();\n    divDhcpViewScopesLoader.show();\n\n    HTTPRequest({\n        url: \"api/dhcp/scopes/disable?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(scopeName) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshDhcpScopes();\n            showAlert(\"success\", \"Scope Disabled!\", \"DHCP Scope was disabled successfully.\");\n        },\n        error: function () {\n            divDhcpViewScopesLoader.hide();\n            divDhcpViewScopes.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDhcpViewScopesLoader\n    });\n}\n\nfunction enableDhcpScope(scopeName) {\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var divDhcpViewScopesLoader = $(\"#divDhcpViewScopesLoader\");\n    var divDhcpViewScopes = $(\"#divDhcpViewScopes\");\n    var divDhcpEditScope = $(\"#divDhcpEditScope\");\n\n    divDhcpViewScopes.hide();\n    divDhcpEditScope.hide();\n    divDhcpViewScopesLoader.show();\n\n    HTTPRequest({\n        url: \"api/dhcp/scopes/enable?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(scopeName) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshDhcpScopes();\n            showAlert(\"success\", \"Scope Enabled!\", \"DHCP Scope was enabled successfully.\");\n        },\n        error: function () {\n            divDhcpViewScopesLoader.hide();\n            divDhcpViewScopes.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDhcpViewScopesLoader\n    });\n}\n\nfunction deleteDhcpScope(index, scopeName) {\n    if (!confirm(\"Are you sure you want to delete the DHCP scope '\" + scopeName + \"'?\"))\n        return;\n\n    var node = $(\"#optDhcpClusterNode\").val();\n\n    var divDhcpViewScopesLoader = $(\"#divDhcpViewScopesLoader\");\n    var divDhcpViewScopes = $(\"#divDhcpViewScopes\");\n    var divDhcpEditScope = $(\"#divDhcpEditScope\");\n\n    divDhcpViewScopes.hide();\n    divDhcpEditScope.hide();\n    divDhcpViewScopesLoader.show();\n\n    HTTPRequest({\n        url: \"api/dhcp/scopes/delete?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(scopeName) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#trDhcpScopeRow\" + index).remove();\n\n            var dhcpLeasesLength = $('#tableDhcpScopesBody >tr').length;\n            if (dhcpLeasesLength > 0)\n                $(\"#tableDhcpScopesFooter\").html(\"<tr><td colspan=\\\"5\\\"><b>Total Scopes: \" + dhcpLeasesLength + \"</b></td></tr>\");\n            else\n                $(\"#tableDhcpScopesFooter\").html(\"<tr><td colspan=\\\"5\\\" align=\\\"center\\\">No Scope Found</td></tr>\");\n\n            divDhcpViewScopes.show();\n            divDhcpViewScopesLoader.hide();\n\n            showAlert(\"success\", \"Scope Deleted!\", \"DHCP Scope was deleted successfully.\");\n        },\n        error: function () {\n            divDhcpViewScopesLoader.hide();\n            divDhcpViewScopes.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDhcpViewScopesLoader\n    });\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/dnsclient.js",
    "content": "/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n$(function () {\n    loadServerList();\n\n    //dropdown list box support\n    $('.dropdown').on('click', 'a', function (e) {\n        e.preventDefault();\n\n        var itemText = $(this).text();\n        $(this).closest('.dropdown').find('input').val(itemText);\n\n        if (itemText.indexOf(\"QUIC\") !== -1)\n            $(\"#optDnsClientProtocol\").val(\"QUIC\");\n        else if ((itemText.indexOf(\"TLS\") !== -1) || (itemText.indexOf(\":853\") !== -1))\n            $(\"#optDnsClientProtocol\").val(\"TLS\");\n        else if ((itemText.indexOf(\"HTTPS\") !== -1) || (itemText.indexOf(\"http://\") !== -1) || (itemText.indexOf(\"https://\") !== -1))\n            $(\"#optDnsClientProtocol\").val(\"HTTPS\");\n        else {\n            switch ($(\"#optDnsClientProtocol\").val()) {\n                case \"UDP\":\n                case \"TCP\":\n                    break;\n\n                default:\n                    $(\"#optDnsClientProtocol\").val(\"UDP\");\n                    break;\n            }\n        }\n    });\n});\n\nfunction loadServerList() {\n    $.ajax({\n        type: \"GET\",\n        url: \"json/dnsclient-server-list-custom.json\",\n        dataType: \"json\",\n        cache: false,\n        async: false,\n        success: function (responseJSON, status, jqXHR) {\n            loadServerListFrom(responseJSON);\n        },\n        error: function (jqXHR, textStatus, errorThrown) {\n            $.ajax({\n                type: \"GET\",\n                url: \"json/dnsclient-server-list-builtin.json\",\n                dataType: \"json\",\n                cache: false,\n                async: false,\n                success: function (responseJSON, status, jqXHR) {\n                    loadServerListFrom(responseJSON);\n                },\n                error: function (jqXHR, textStatus, errorThrown) {\n                    showAlert(\"danger\", \"Error!\", \"Failed to load server list: \" + jqXHR.status + \" \" + jqXHR.statusText);\n                }\n            });\n        }\n    });\n}\n\nfunction loadServerListFrom(responseJSON) {\n    $(\"#txtDnsClientNameServer\").val(\"This Server {this-server}\");\n\n    var htmlList = \"<li><a href=\\\"#\\\">This Server {this-server}</a></li>\";\n\n    for (var i = 0; i < responseJSON.length; i++) {\n        for (var j = 0; j < responseJSON[i].addresses.length; j++) {\n            if ((responseJSON[i].name == null) || (responseJSON[i].name.length == 0))\n                htmlList += \"<li><a href=\\\"#\\\">\" + htmlEncode(responseJSON[i].addresses[j]) + \"</a></li>\";\n            else\n                htmlList += \"<li><a href=\\\"#\\\">\" + htmlEncode(responseJSON[i].name) + \" {\" + htmlEncode(responseJSON[i].addresses[j]) + \"}</a></li>\";\n        }\n    }\n\n    $(\"#optDnsClientNameServers\").html(htmlList);\n}\n\nfunction resolveQuery(importRecords) {\n    if (importRecords == null)\n        importRecords = false;\n\n    var server = $(\"#txtDnsClientNameServer\").val();\n\n    if ((server.indexOf(\"recursive-resolver\") !== -1) || (server.indexOf(\"system-dns\") !== -1))\n        $(\"#optDnsClientProtocol\").val(\"UDP\");\n\n    var domain = $(\"#txtDnsClientDomain\").val();\n    var type = $(\"#optDnsClientType\").val();\n    var protocol = $(\"#optDnsClientProtocol\").val();\n    var dnssecValidation = $(\"#chkDnsClientDnssecValidation\").prop(\"checked\");\n    var eDnsClientSubnet = $(\"#txtDnsClientEDnsClientSubnet\").val();\n\n    {\n        var i = server.indexOf(\"{\");\n        if (i > -1) {\n            var j = server.lastIndexOf(\"}\");\n            server = server.substring(i + 1, j);\n        }\n    }\n\n    server = server.trim();\n\n    if ((server === null) || (server === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a valid Name Server.\");\n        $(\"#txtDnsClientNameServer\").trigger(\"focus\");\n        return;\n    }\n\n    if ((domain === null) || (domain === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to query.\");\n        $(\"#txtDnsClientDomain\").trigger(\"focus\");\n        return;\n    }\n\n    {\n        var i = domain.indexOf(\"://\");\n        if (i > -1) {\n            var j = domain.indexOf(\":\", i + 3);\n\n            if (j < 0)\n                j = domain.indexOf(\"/\", i + 3);\n\n            if (j > -1)\n                domain = domain.substring(i + 3, j);\n            else\n                domain = domain.substring(i + 3);\n\n            $(\"#txtDnsClientDomain\").val(domain);\n        }\n    }\n\n    if (importRecords) {\n        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?\"))\n            return;\n    }\n\n    var node = $(\"#optDnsClientClusterNode\").val();\n\n    var btn = $(importRecords ? \"#btnDnsClientImport\" : \"#btnDnsClientResolve\").button(\"loading\");\n    var btnOther = $(importRecords ? \"#btnDnsClientResolve\" : \"#btnDnsClientImport\").prop(\"disabled\", true);\n\n    var divDnsClientLoader = $(\"#divDnsClientLoader\");\n    var divDnsClientOutputAccordion = $(\"#divDnsClientOutputAccordion\");\n\n    divDnsClientOutputAccordion.hide();\n    divDnsClientLoader.show();\n\n    HTTPRequest({\n        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),\n        success: function (responseJSON) {\n            divDnsClientLoader.hide();\n            btn.button(\"reset\");\n            btnOther.prop(\"disabled\", false);\n\n            $(\"#preDnsClientFinalResponse\").text(JSON.stringify(responseJSON.response.result, null, 2));\n            $(\"#divDnsClientFinalResponseCollapse\").collapse(\"show\");\n            $(\"#divDnsClientRawResponsesCollapse\").collapse(\"hide\");\n            divDnsClientOutputAccordion.show();\n\n            if ((responseJSON.response.rawResponses != null)) {\n                if (responseJSON.response.rawResponses.length == 0) {\n                    $(\"#divDnsClientRawResponsePanel\").hide();\n                }\n                else {\n                    var rawListHtml = \"\";\n\n                    for (var i = 0; i < responseJSON.response.rawResponses.length; i++) {\n                        rawListHtml += \"<li class=\\\"list-group-item\\\"><pre style=\\\"margin-top: 5px; margin-bottom: 5px;\\\">\" + JSON.stringify(responseJSON.response.rawResponses[i], null, 2) + \"</pre></li>\";\n                    }\n\n                    $(\"#spanDnsClientRawResponsesCount\").text(responseJSON.response.rawResponses.length);\n                    $(\"#ulDnsClientRawResponsesList\").html(rawListHtml);\n                    $(\"#divDnsClientRawResponsesCollapse\").collapse(\"hide\");\n                    $(\"#divDnsClientRawResponsePanel\").show();\n                }\n            }\n\n            if (responseJSON.response.warningMessage != null) {\n                showAlert(\"warning\", \"Warning!\", responseJSON.response.warningMessage);\n            }\n            else if (importRecords) {\n                showAlert(\"success\", \"Records Imported!\", \"Resource records resolved by this DNS client query were successfully imported into this server.\");\n            }\n        },\n        error: function () {\n            divDnsClientLoader.hide();\n            btn.button(\"reset\");\n            btnOther.prop(\"disabled\", false);\n        },\n        invalidToken: function () {\n            divDnsClientLoader.hide();\n            btn.button(\"reset\");\n            btnOther.prop(\"disabled\", false);\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDnsClientLoader,\n        showInnerError: true\n    });\n\n    //add server name to list if doesnt exists\n    var txtServerName = $(\"#txtDnsClientNameServer\").val();\n    var containsServer = false;\n\n    $(\"#optDnsClientNameServers a\").each(function () {\n        if ($(this).html() === txtServerName)\n            containsServer = true;\n    });\n\n    if (!containsServer)\n        $(\"#optDnsClientNameServers\").prepend(\"<li><a href=\\\"#\\\">\" + htmlEncode(txtServerName) + \"</a></li>\");\n}\n\nfunction queryDnsServer(domain, type, node) {\n    if (type == null)\n        type = \"A\";\n\n    $(\"#txtDnsClientNameServer\").val(\"This Server {this-server}\");\n    $(\"#txtDnsClientDomain\").val(domain);\n    $(\"#optDnsClientType\").val(type);\n    $(\"#optDnsClientProtocol\").val(\"UDP\");\n    $(\"#txtDnsClientEDnsClientSubnet\").val(\"\");\n    $(\"#chkDnsClientDnssecValidation\").prop(\"checked\", false);\n\n    if ((node != null) && (node != \"cluster\"))\n        $(\"#optDnsClientClusterNode\").val(node);\n\n    $(\"#mainPanelTabListDashboard\").removeClass(\"active\");\n    $(\"#mainPanelTabPaneDashboard\").removeClass(\"active\");\n\n    $(\"#mainPanelTabListLogs\").removeClass(\"active\");\n    $(\"#mainPanelTabPaneLogs\").removeClass(\"active\");\n\n    $(\"#mainPanelTabListDnsClient\").addClass(\"active\");\n    $(\"#mainPanelTabPaneDnsClient\").addClass(\"active\");\n\n    $(\"#modalTopStats\").modal(\"hide\");\n\n    resolveQuery();\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/logs.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n$(function () {\n    $(\"#optQueryLogsAppName\").on(\"change\", function () {\n        if (appsList == null)\n            return;\n\n        var appName = $(\"#optQueryLogsAppName\").val();\n        var optClassPaths = \"\";\n\n        for (var i = 0; i < appsList.length; i++) {\n            if (appsList[i].name == appName) {\n                for (var j = 0; j < appsList[i].dnsApps.length; j++) {\n                    if (appsList[i].dnsApps[j].isQueryLogs)\n                        optClassPaths += \"<option>\" + appsList[i].dnsApps[j].classPath + \"</option>\";\n                }\n\n                break;\n            }\n        }\n\n        $(\"#optQueryLogsClassPath\").html(optClassPaths);\n        $(\"#txtAddEditRecordDataData\").val(\"\");\n    });\n\n    $(\"#optQueryLogsEntriesPerPage\").on(\"change\", function () {\n        localStorage.setItem(\"optQueryLogsEntriesPerPage\", $(\"#optQueryLogsEntriesPerPage\").val());\n    });\n\n    var optQueryLogsEntriesPerPage = localStorage.getItem(\"optQueryLogsEntriesPerPage\");\n    if (optQueryLogsEntriesPerPage != null)\n        $(\"#optQueryLogsEntriesPerPage\").val(optQueryLogsEntriesPerPage);\n});\n\nfunction refreshLogsTab() {\n    if ($(\"#logsTabListLogViewer\").hasClass(\"active\"))\n        refreshLogFilesList();\n    else if ($(\"#logsTabListQueryLogs\").hasClass(\"active\"))\n        refreshQueryLogsTab();\n}\n\nfunction logsClusterNodeChanged() {\n    if ($(\"#logsTabListLogViewer\").hasClass(\"active\")) {\n        if ($(\"#divLogViewer\").is(\":visible\"))\n            refreshLogFilesList($(\"#txtLogViewerTitle\").text());\n        else\n            refreshLogFilesList();\n    }\n    else if ($(\"#logsTabListQueryLogs\").hasClass(\"active\")) {\n        refreshQueryLogsTab();\n\n        if ($(\"#divQueryLogsTable\").is(\":visible\"))\n            queryLogs();\n    }\n}\n\nfunction refreshLogFilesList(selectedFileName) {\n    var lstLogFiles = $(\"#lstLogFiles\");\n\n    var node = $(\"#optLogsClusterNode\").val();\n\n    HTTPRequest({\n        url: \"api/logs/list?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var logFiles = responseJSON.response.logFiles;\n\n            var list = \"<div class=\\\"log\\\" style=\\\"font-size: 14px; padding-bottom: 6px;\\\"><a href=\\\"#\\\" onclick=\\\"deleteAllStats(); return false;\\\"><b>[delete all stats]</b></a></div>\";\n\n            if (logFiles.length == 0) {\n                list += \"<div class=\\\"log\\\">No Log File Was Found</div>\";\n            }\n            else {\n                list += \"<div class=\\\"log\\\" style=\\\"font-size: 14px; padding-bottom: 6px;\\\"><a href=\\\"#\\\" onclick=\\\"deleteAllLogs(); return false;\\\"><b>[delete all logs]</b></a></div>\";\n\n                for (var i = 0; i < logFiles.length; i++) {\n                    var logFile = logFiles[i];\n\n                    list += \"<div class=\\\"log\\\"><a href=\\\"#\\\" onclick=\\\"viewLog('\" + logFile.fileName + \"'); return false;\\\">\" + logFile.fileName + \" [\" + logFile.size + \"]</a></div>\"\n                }\n            }\n\n            lstLogFiles.html(list);\n\n            if (selectedFileName != null) {\n                for (var i = 0; i < logFiles.length; i++) {\n                    if (logFiles[i].fileName == selectedFileName) {\n                        viewLog(selectedFileName);\n                        return;\n                    }\n                }\n\n                //selected file not found\n                $(\"#divLogViewer\").hide();\n            }\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: lstLogFiles\n    });\n}\n\nfunction viewLog(logFile) {\n    var divLogViewer = $(\"#divLogViewer\");\n    var txtLogViewerTitle = $(\"#txtLogViewerTitle\");\n    var divLogViewerLoader = $(\"#divLogViewerLoader\");\n    var preLogViewerBody = $(\"#preLogViewerBody\");\n\n    txtLogViewerTitle.text(logFile);\n\n    var node = $(\"#optLogsClusterNode\").val();\n\n    preLogViewerBody.hide();\n    divLogViewerLoader.show();\n    divLogViewer.show();\n\n    HTTPRequest({\n        url: \"api/logs/download?token=\" + sessionData.token + \"&fileName=\" + encodeURIComponent(logFile) + \"&limit=2\" + \"&node=\" + encodeURIComponent(node),\n        isTextResponse: true,\n        success: function (response) {\n            divLogViewerLoader.hide();\n\n            if (response.status != null)\n                response = JSON.stringify(response, null, 2);\n\n            preLogViewerBody.text(response);\n            preLogViewerBody.show();\n        },\n        objLoaderPlaceholder: divLogViewerLoader\n    });\n}\n\nfunction downloadLog() {\n    var logFile = $(\"#txtLogViewerTitle\").text();\n    var node = $(\"#optLogsClusterNode\").val();\n\n    window.open(\"api/logs/download?token=\" + sessionData.token + \"&fileName=\" + encodeURIComponent(logFile) + \"&node=\" + encodeURIComponent(node) + \"&ts=\" + (new Date().getTime()), \"_blank\");\n}\n\nfunction deleteLog() {\n    var logFile = $(\"#txtLogViewerTitle\").text();\n\n    if (!confirm(\"Are you sure you want to permanently delete the log file '\" + logFile + \"'?\"))\n        return;\n\n    var node = $(\"#optLogsClusterNode\").val();\n\n    var btn = $(\"#btnDeleteLog\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/logs/delete?token=\" + sessionData.token + \"&log=\" + encodeURIComponent(logFile) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshLogFilesList();\n\n            $(\"#divLogViewer\").hide();\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Log Deleted!\", \"Log file was deleted successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteAllLogs() {\n    if (!confirm(\"Are you sure you want to permanently delete all log files?\"))\n        return;\n\n    var node = $(\"#optLogsClusterNode\").val();\n\n    HTTPRequest({\n        url: \"api/logs/deleteAll?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshLogFilesList();\n\n            $(\"#divLogViewer\").hide();\n\n            showAlert(\"success\", \"Logs Deleted!\", \"All log files were deleted successfully.\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteAllStats() {\n    if (!confirm(\"Are you sure you want to permanently delete all stats files?\"))\n        return;\n\n    var node = $(\"#optLogsClusterNode\").val();\n\n    HTTPRequest({\n        url: \"api/dashboard/stats/deleteAll?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            showAlert(\"success\", \"Stats Deleted!\", \"All stats files were deleted successfully.\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nvar appsList;\n\nfunction refreshQueryLogsTab(doQueryLogs) {\n    var frmQueryLogs = $(\"#frmQueryLogs\");\n    var divQueryLogsLoader = $(\"#divQueryLogsLoader\");\n\n    var optQueryLogsAppName = $(\"#optQueryLogsAppName\");\n    var optQueryLogsClassPath = $(\"#optQueryLogsClassPath\");\n\n    var currentAppName = optQueryLogsAppName.val();\n    var currentClassPath = optQueryLogsClassPath.val();\n    var loader;\n\n    if (appsList == null) {\n        frmQueryLogs.hide();\n        loader = divQueryLogsLoader;\n    }\n    else {\n        optQueryLogsAppName.prop(\"disabled\", true);\n        optQueryLogsClassPath.prop(\"disabled\", true);\n    }\n\n    HTTPRequest({\n        url: \"api/apps/list?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var apps = responseJSON.response.apps;\n\n            var optApps = \"\";\n            var optClassPaths = \"\";\n\n            for (var i = 0; i < apps.length; i++) {\n                for (var j = 0; j < apps[i].dnsApps.length; j++) {\n                    if (apps[i].dnsApps[j].isQueryLogs) {\n                        optApps += \"<option>\" + apps[i].name + \"</option>\";\n\n                        if (currentAppName == null)\n                            currentAppName = apps[i].name;\n\n                        break;\n                    }\n                }\n            }\n\n            for (var i = 0; i < apps.length; i++) {\n                if (apps[i].name == currentAppName) {\n                    for (var j = 0; j < apps[i].dnsApps.length; j++) {\n                        if (apps[i].dnsApps[j].isQueryLogs)\n                            optClassPaths += \"<option>\" + apps[i].dnsApps[j].classPath + \"</option>\";\n                    }\n\n                    break;\n                }\n            }\n\n            optQueryLogsAppName.html(optApps);\n            optQueryLogsClassPath.html(optClassPaths);\n\n            if (currentAppName != null)\n                optQueryLogsAppName.val(currentAppName);\n\n            if (currentClassPath != null)\n                optQueryLogsClassPath.val(currentClassPath);\n\n            if (appsList == null) {\n                frmQueryLogs.show();\n                loader.hide();\n            }\n            else {\n                optQueryLogsAppName.prop(\"disabled\", false);\n                optQueryLogsClassPath.prop(\"disabled\", false);\n            }\n\n            appsList = apps;\n\n            if (doQueryLogs)\n                queryLogs();\n        },\n        error: function () {\n            if (appsList == null) {\n                frmQueryLogs.show();\n            }\n            else {\n                optQueryLogsAppName.prop(\"disabled\", false);\n                optQueryLogsClassPath.prop(\"disabled\", false);\n            }\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: loader\n    });\n}\n\nfunction queryLogs(pageNumber) {\n    var btn = $(\"#btnQueryLogs\");\n    var divQueryLogsLoader = $(\"#divQueryLogsLoader\");\n    var divQueryLogsTable = $(\"#divQueryLogsTable\");\n\n    var name = $(\"#optQueryLogsAppName\").val();\n    if (name == null) {\n        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.\");\n        $(\"#optQueryLogsAppName\").trigger(\"focus\");\n        return false;\n    }\n\n    var classPath = $(\"#optQueryLogsClassPath\").val();\n    if (classPath == null) {\n        showAlert(\"warning\", \"Missing!\", \"Please select a Class Path to query logs.\");\n        $(\"#optQueryLogsClassPath\").trigger(\"focus\");\n        return false;\n    }\n\n    if (pageNumber == null)\n        pageNumber = $(\"#txtQueryLogPageNumber\").val();\n\n    var entriesPerPage = Number($(\"#optQueryLogsEntriesPerPage\").val());\n    if (entriesPerPage < 1)\n        entriesPerPage = 10;\n\n    var descendingOrder = $(\"#optQueryLogsDescendingOrder\").val();\n\n    var start = $(\"#txtQueryLogStart\").val();\n    if (start != \"\")\n        start = moment(start).toISOString();\n\n    var end = $(\"#txtQueryLogEnd\").val();\n    if (end != \"\")\n        end = moment(end).toISOString();\n\n    var clientIpAddress = $(\"#txtQueryLogClientIpAddress\").val();\n    var protocol = $(\"#optQueryLogsProtocol\").val();\n    var responseType = $(\"#optQueryLogsResponseType\").val();\n    var rcode = $(\"#optQueryLogsResponseCode\").val();\n    var qname = $(\"#txtQueryLogQName\").val();\n    var qtype = $(\"#txtQueryLogQType\").val();\n    var qclass = $(\"#optQueryLogQClass\").val();\n\n    var node = $(\"#optLogsClusterNode\").val();\n\n    divQueryLogsTable.hide();\n    divQueryLogsLoader.show();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/logs/query?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(name) + \"&classPath=\" + encodeURIComponent(classPath) + \"&pageNumber=\" + pageNumber + \"&entriesPerPage=\" + entriesPerPage + \"&descendingOrder=\" + descendingOrder +\n            \"&start=\" + encodeURIComponent(start) + \"&end=\" + encodeURIComponent(end) + \"&clientIpAddress=\" + encodeURIComponent(clientIpAddress) + \"&protocol=\" + protocol + \"&responseType=\" + responseType + \"&rcode=\" + rcode +\n            \"&qname=\" + encodeURIComponent(qname) + \"&qtype=\" + qtype + \"&qclass=\" + qclass +\n            \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var tableHtml = \"\";\n\n            for (var i = 0; i < responseJSON.response.entries.length; i++) {\n                var trbgcolor;\n\n                switch (responseJSON.response.entries[i].rcode.toLowerCase()) {\n                    case \"serverfailure\":\n                        trbgcolor = \"rgba(217, 83, 79, 0.1)\";\n                        break;\n\n                    case \"nxdomain\":\n                        switch (responseJSON.response.entries[i].responseType.toLowerCase()) {\n                            case \"blocked\":\n                            case \"upstreamblocked\":\n                            case \"upstreamblockedcached\":\n                                trbgcolor = \"rgba(255, 165, 0, 0.1)\";\n                                break;\n\n                            default:\n                                trbgcolor = \"rgba(120, 120, 120, 0.1)\";\n                                break;\n                        }\n\n                        break;\n\n                    case \"refused\":\n                        trbgcolor = \"rgba(91, 192, 222, 0.1)\";\n                        break;\n\n                    default:\n                        switch (responseJSON.response.entries[i].responseType.toLowerCase()) {\n                            case \"authoritative\":\n                                trbgcolor = \"rgba(150, 150, 0, 0.1)\";\n                                break;\n\n                            case \"recursive\":\n                                trbgcolor = \"rgba(23, 162, 184, 0.1)\";\n                                break;\n\n                            case \"cached\":\n                                trbgcolor = \"rgba(111, 84, 153, 0.1)\";\n                                break;\n\n                            case \"blocked\":\n                            case \"upstreamblocked\":\n                            case \"upstreamblockedcached\":\n                                trbgcolor = \"rgba(255, 165, 0, 0.1)\";\n                                break;\n\n                            default:\n                                trbgcolor = null;\n                                break;\n                        }\n\n                        break;\n                }\n\n                tableHtml += \"<tr\" + (trbgcolor == null ? \"\" : \" style=\\\"background-color: \" + trbgcolor + \";\\\"\") + \"><td>\" + responseJSON.response.entries[i].rowNumber + \"</td><td>\" +\n                    moment(responseJSON.response.entries[i].timestamp).local().format(\"YYYY-MM-DD HH:mm:ss\") + \"</td><td style=\\\"word-break: break-all; min-width: 125px;\\\">\" +\n                    responseJSON.response.entries[i].clientIpAddress + \"</td><td>\" +\n                    responseJSON.response.entries[i].protocol + \"</td><td>\" +\n                    responseJSON.response.entries[i].responseType + (responseJSON.response.entries[i].responseRtt == null ? \"\" : \"<div style=\\\"font-size: 12px;\\\">(\" + responseJSON.response.entries[i].responseRtt.toFixed(2) + \" ms)</div>\") + \"</td><td>\" +\n                    responseJSON.response.entries[i].rcode + \"</td><td style=\\\"word-break: break-all;\\\">\" +\n                    htmlEncode(responseJSON.response.entries[i].qname == \"\" ? \".\" : responseJSON.response.entries[i].qname) + \"</td><td>\" +\n                    (responseJSON.response.entries[i].qtype == null ? \"\" : responseJSON.response.entries[i].qtype) + \"</td><td>\" +\n                    (responseJSON.response.entries[i].qclass == null ? \"\" : responseJSON.response.entries[i].qclass) + \"</td><td style=\\\"word-break: break-all;\\\">\" +\n                    htmlEncode(responseJSON.response.entries[i].answer) +\n                    \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnQueryLogsRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n\n                tableHtml += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"queryDnsServer('\" + responseJSON.response.entries[i].qname + \"', '\" + responseJSON.response.entries[i].qtype + \"', '\" + node + \"'); return false;\\\">Query DNS Server</a></li>\";\n\n                switch (responseJSON.response.entries[i].responseType.toLowerCase()) {\n                    case \"blocked\":\n                    case \"upstreamblocked\":\n                    case \"upstreamblockedcached\":\n                        tableHtml += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-domain=\\\"\" + htmlEncode(responseJSON.response.entries[i].qname) + \"\\\" onclick=\\\"allowDomain(this, 'btnQueryLogsRowOption'); return false;\\\">Allow Domain</a></li>\";\n                        break;\n\n                    default:\n                        tableHtml += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-domain=\\\"\" + htmlEncode(responseJSON.response.entries[i].qname) + \"\\\" onclick=\\\"blockDomain(this, 'btnQueryLogsRowOption'); return false;\\\">Block Domain</a></li>\";\n                        break;\n                }\n\n                tableHtml += \"</ul></div></td></tr>\";\n            }\n\n            var paginationHtml = \"\";\n\n            if (responseJSON.response.pageNumber > 1) {\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"First\\\" onClick=\\\"queryLogs(1); return false;\\\"><span aria-hidden=\\\"true\\\">&laquo;</span></a></li>\";\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Previous\\\" onClick=\\\"queryLogs(\" + (responseJSON.response.pageNumber - 1) + \"); return false;\\\"><span aria-hidden=\\\"true\\\">&lsaquo;</span></a></li>\";\n            }\n\n            var pageStart = responseJSON.response.pageNumber - 5;\n            if (pageStart < 1)\n                pageStart = 1;\n\n            var pageEnd = pageStart + 9;\n            if (pageEnd > responseJSON.response.totalPages) {\n                var endDiff = pageEnd - responseJSON.response.totalPages;\n                pageEnd = responseJSON.response.totalPages;\n\n                pageStart -= endDiff;\n                if (pageStart < 1)\n                    pageStart = 1;\n            }\n\n            for (var i = pageStart; i <= pageEnd; i++) {\n                if (i == responseJSON.response.pageNumber)\n                    paginationHtml += \"<li class=\\\"active\\\"><a href=\\\"#\\\" onClick=\\\"queryLogs(\" + i + \"); return false;\\\">\" + i + \"</a></li>\";\n                else\n                    paginationHtml += \"<li><a href=\\\"#\\\" onClick=\\\"queryLogs(\" + i + \"); return false;\\\">\" + i + \"</a></li>\";\n            }\n\n            if (responseJSON.response.pageNumber < responseJSON.response.totalPages) {\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Next\\\" onClick=\\\"queryLogs(\" + (responseJSON.response.pageNumber + 1) + \"); return false;\\\"><span aria-hidden=\\\"true\\\">&rsaquo;</span></a></li>\";\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Last\\\" onClick=\\\"queryLogs(-1); return false;\\\"><span aria-hidden=\\\"true\\\">&raquo;</span></a></li>\";\n            }\n\n            $(\"#tableQueryLogsBody\").html(tableHtml);\n\n            var statusHtml;\n\n            if (responseJSON.response.entries.length > 0)\n                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 + \")\";\n            else\n                statusHtml = \"0 logs\";\n\n            $(\"#tableQueryLogsTopStatus\").html(statusHtml);\n            $(\"#tableQueryLogsTopPagination\").html(paginationHtml);\n\n            $(\"#tableQueryLogsFooterStatus\").html(statusHtml);\n            $(\"#tableQueryLogsFooterPagination\").html(paginationHtml);\n\n            btn.button(\"reset\");\n            divQueryLogsLoader.hide();\n            divQueryLogsTable.show();\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divQueryLogsLoader\n    });\n}\n\nfunction showQueryLogs(domain, clientIp, node) {\n    $(\"#frmQueryLogs\").trigger(\"reset\");\n\n    if (domain != null)\n        $(\"#txtQueryLogQName\").val(domain);\n\n    if (clientIp != null)\n        $(\"#txtQueryLogClientIpAddress\").val(clientIp);\n\n    if ((node != null) && (node != \"cluster\"))\n        $(\"#optLogsClusterNode\").val(node);\n\n    $(\"#mainPanelTabListDashboard\").removeClass(\"active\");\n    $(\"#mainPanelTabPaneDashboard\").removeClass(\"active\");\n\n    $(\"#mainPanelTabListLogs\").addClass(\"active\");\n    $(\"#mainPanelTabPaneLogs\").addClass(\"active\");\n\n    $(\"#logsTabListLogViewer\").removeClass(\"active\");\n    $(\"#logsTabPaneLogViewer\").removeClass(\"active\");\n\n    $(\"#logsTabListQueryLogs\").addClass(\"active\");\n    $(\"#logsTabPaneQueryLogs\").addClass(\"active\");\n\n    $(\"#modalTopStats\").modal(\"hide\");\n\n    refreshQueryLogsTab(true);\n}\n\nfunction exportQueryLogsCsv() {\n    var name = $(\"#optQueryLogsAppName\").val();\n    if (name == null) {\n        showAlert(\"warning\", \"Missing!\", \"Please install the 'Query Logs (Sqlite)' DNS App or any other DNS app that supports query logging feature.\");\n        $(\"#optQueryLogsAppName\").trigger(\"focus\");\n        return false;\n    }\n\n    var classPath = $(\"#optQueryLogsClassPath\").val();\n    if (classPath == null) {\n        showAlert(\"warning\", \"Missing!\", \"Please select a Class Path to query logs.\");\n        $(\"#optQueryLogsClassPath\").trigger(\"focus\");\n        return false;\n    }\n\n    var start = $(\"#txtQueryLogStart\").val();\n    if (start != \"\")\n        start = moment(start).toISOString();\n\n    var end = $(\"#txtQueryLogEnd\").val();\n    if (end != \"\")\n        end = moment(end).toISOString();\n\n    var clientIpAddress = $(\"#txtQueryLogClientIpAddress\").val();\n    var protocol = $(\"#optQueryLogsProtocol\").val();\n    var responseType = $(\"#optQueryLogsResponseType\").val();\n    var rcode = $(\"#optQueryLogsResponseCode\").val();\n    var qname = $(\"#txtQueryLogQName\").val();\n    var qtype = $(\"#txtQueryLogQType\").val();\n    var qclass = $(\"#optQueryLogQClass\").val();\n\n    var node = $(\"#optLogsClusterNode\").val();\n\n    window.open(\"api/logs/export?token=\" + sessionData.token + \"&name=\" + encodeURIComponent(name) + \"&classPath=\" + encodeURIComponent(classPath) +\n        \"&start=\" + encodeURIComponent(start) + \"&end=\" + encodeURIComponent(end) + \"&clientIpAddress=\" + encodeURIComponent(clientIpAddress) +\n        \"&protocol=\" + protocol + \"&responseType=\" + responseType + \"&rcode=\" + rcode + \"&qname=\" + encodeURIComponent(qname) + \"&qtype=\" + qtype + \"&qclass=\" + qclass +\n        \"&node=\" + encodeURIComponent(node)\n        , \"_blank\");\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/main.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nvar refreshTimerHandle;\nvar reverseProxyDetected = false;\nvar quickBlockLists = null;\nvar quickForwardersList = null;\n\nfunction showPageLogin() {\n    hideAlert();\n\n    localStorage.removeItem(\"token\");\n\n    $(\"#pageMain\").hide();\n    $(\"#mnuUser\").hide();\n\n    $(\"#txtUser\").val(\"\");\n    $(\"#txtPass\").val(\"\");\n    $(\"#txtPass\").prop(\"disabled\", false);\n    $(\"#div2FAOTP\").hide();\n    $(\"#txt2FATOTP\").val(\"\");\n    $(\"#btnLogin\").button(\"reset\");\n    $(\"#pageLogin\").show();\n\n    $(\"#txtUser\").trigger(\"focus\");\n\n    if (refreshTimerHandle != null) {\n        clearInterval(refreshTimerHandle);\n        refreshTimerHandle = null;\n    }\n}\n\nfunction showPageMain() {\n    hideAlert();\n\n    $(\"#pageLogin\").hide();\n    $(\"#mnuUser\").show();\n\n    $(\".nav-tabs li\").removeClass(\"active\");\n    $(\".tab-pane\").removeClass(\"active\");\n    $(\"#mainPanelTabListDashboard\").addClass(\"active\");\n    $(\"#mainPanelTabPaneDashboard\").addClass(\"active\");\n    $(\"#settingsTabListGeneral\").addClass(\"active\");\n    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n    $(\"#dhcpTabListLeases\").addClass(\"active\");\n    $(\"#dhcpTabPaneLeases\").addClass(\"active\");\n    $(\"#adminTabListSessions\").addClass(\"active\");\n    $(\"#adminTabPaneSessions\").addClass(\"active\");\n    $(\"#logsTabListLogViewer\").addClass(\"active\");\n    $(\"#logsTabPaneLogViewer\").addClass(\"active\");\n\n    $(\"#divViewZones\").show();\n    $(\"#divEditZone\").hide();\n\n    $(\"#divDhcpViewScopes\").show();\n    $(\"#divDhcpEditScope\").hide();\n\n    $(\"#txtDnsClientNameServer\").val(\"This Server {this-server}\");\n    $(\"#txtDnsClientDomain\").val(\"\");\n    $(\"#optDnsClientType\").val(\"A\");\n    $(\"#optDnsClientProtocol\").val(\"UDP\");\n    $(\"#txtDnsClientEDnsClientSubnet\").val(\"\");\n    $(\"#chkDnsClientDnssecValidation\").prop(\"checked\", false);\n    $(\"#divDnsClientLoader\").hide();\n    $(\"#preDnsClientFinalResponse\").text(\"\");\n    $(\"#divDnsClientOutputAccordion\").hide();\n\n    $(\"#divLogViewer\").hide();\n    $(\"#divQueryLogsTable\").hide();\n\n    updateAllClusterNodeDropDowns();\n\n    if (sessionData.info.permissions.Dashboard.canView) {\n        $(\"#mainPanelTabListDashboard\").show();\n        refreshDashboard();\n    }\n    else {\n        $(\"#mainPanelTabListDashboard\").hide();\n\n        $(\"#mainPanelTabListDashboard\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneDashboard\").removeClass(\"active\");\n\n        if (sessionData.info.permissions.Zones.canView) {\n            $(\"#mainPanelTabListZones\").addClass(\"active\");\n            $(\"#mainPanelTabPaneZones\").addClass(\"active\");\n            refreshZones(true);\n        }\n        else if (sessionData.info.permissions.Cache.canView) {\n            $(\"#mainPanelTabListCachedZones\").addClass(\"active\");\n            $(\"#mainPanelTabPaneCachedZones\").addClass(\"active\");\n        }\n        else if (sessionData.info.permissions.Allowed.canView) {\n            $(\"#mainPanelTabListAllowedZones\").addClass(\"active\");\n            $(\"#mainPanelTabPaneAllowedZones\").addClass(\"active\");\n        }\n        else if (sessionData.info.permissions.Blocked.canView) {\n            $(\"#mainPanelTabListBlockedZones\").addClass(\"active\");\n            $(\"#mainPanelTabPaneBlockedZones\").addClass(\"active\");\n        }\n        else if (sessionData.info.permissions.Apps.canView) {\n            $(\"#mainPanelTabListApps\").addClass(\"active\");\n            $(\"#mainPanelTabPaneApps\").addClass(\"active\");\n            refreshApps();\n        }\n        else if (sessionData.info.permissions.DnsClient.canView) {\n            $(\"#mainPanelTabListDnsClient\").addClass(\"active\");\n            $(\"#mainPanelTabPaneDnsClient\").addClass(\"active\");\n        }\n        else if (sessionData.info.permissions.Settings.canView) {\n            $(\"#mainPanelTabListSettings\").addClass(\"active\");\n            $(\"#mainPanelTabPaneSettings\").addClass(\"active\");\n            refreshDnsSettings()\n        }\n        else if (sessionData.info.permissions.DhcpServer.canView) {\n            $(\"#mainPanelTabListDhcp\").addClass(\"active\");\n            $(\"#mainPanelTabPaneDhcp\").addClass(\"active\");\n            refreshDhcpTab();\n        }\n        else if (sessionData.info.permissions.Administration.canView) {\n            $(\"#mainPanelTabListAdmin\").addClass(\"active\");\n            $(\"#mainPanelTabPaneAdmin\").addClass(\"active\");\n            refreshAdminTab();\n        }\n        else if (sessionData.info.permissions.Logs.canView) {\n            $(\"#mainPanelTabListLogs\").addClass(\"active\");\n            $(\"#mainPanelTabPaneLogs\").addClass(\"active\");\n            refreshLogsTab();\n        }\n        else {\n            $(\"#mainPanelTabListAbout\").addClass(\"active\");\n            $(\"#mainPanelTabPaneAbout\").addClass(\"active\");\n        }\n    }\n\n    if (sessionData.info.permissions.Zones.canView) {\n        $(\"#mainPanelTabListZones\").show();\n    }\n    else {\n        $(\"#mainPanelTabListZones\").hide();\n    }\n\n    if (sessionData.info.permissions.Cache.canView) {\n        $(\"#mainPanelTabListCachedZones\").show();\n        refreshCachedZonesList(\"\");\n    }\n    else {\n        $(\"#mainPanelTabListCachedZones\").hide();\n    }\n\n    if (sessionData.info.permissions.Allowed.canView) {\n        $(\"#mainPanelTabListAllowedZones\").show();\n        refreshAllowedZonesList(\"\");\n    }\n    else {\n        $(\"#mainPanelTabListAllowedZones\").hide();\n    }\n\n    if (sessionData.info.permissions.Blocked.canView) {\n        $(\"#mainPanelTabListBlockedZones\").show();\n        refreshBlockedZonesList(\"\");\n    }\n    else {\n        $(\"#mainPanelTabListBlockedZones\").hide();\n    }\n\n    if (sessionData.info.permissions.Apps.canView) {\n        $(\"#mainPanelTabListApps\").show();\n    }\n    else {\n        $(\"#mainPanelTabListApps\").hide();\n    }\n\n    if (sessionData.info.permissions.DnsClient.canView) {\n        $(\"#mainPanelTabListDnsClient\").show();\n    }\n    else {\n        $(\"#mainPanelTabListDnsClient\").hide();\n    }\n\n    if (sessionData.info.permissions.Settings.canView) {\n        $(\"#mainPanelTabListSettings\").show();\n    }\n    else {\n        $(\"#mainPanelTabListSettings\").hide();\n    }\n\n    if (sessionData.info.permissions.DhcpServer.canView) {\n        $(\"#mainPanelTabListDhcp\").show();\n    }\n    else {\n        $(\"#mainPanelTabListDhcp\").hide();\n    }\n\n    if (sessionData.info.permissions.Administration.canView) {\n        $(\"#mainPanelTabListAdmin\").show();\n    }\n    else {\n        $(\"#mainPanelTabListAdmin\").hide();\n    }\n\n    if (sessionData.info.permissions.Logs.canView) {\n        $(\"#mainPanelTabListLogs\").show();\n    }\n    else {\n        $(\"#mainPanelTabListLogs\").hide();\n    }\n\n    $(\"#pageMain\").show();\n\n    checkForUpdate();\n\n    refreshTimerHandle = setInterval(function () {\n        var type = $(\"input[name=rdStatType]:checked\").val();\n        if (type === \"lastHour\")\n            refreshDashboard(true);\n\n        $(\"#lblAboutUptime\").text(moment(sessionData.info.uptimestamp).local().format(\"lll\") + \" (\" + moment(sessionData.info.uptimestamp).fromNow() + \")\");\n    }, 60000);\n}\n\n$(function () {\n    var headerHtml = $(\"#header\").html();\n\n    $(\"#header\").html(\"<div class=\\\"title\\\"><a href=\\\".\\\"><img src=\\\"img/logo25x25.png\\\" alt=\\\"Technitium Logo\\\" /><span class=\\\"text\\\" style=\\\"color: #ffffff;\\\">Technitium</span></a>\" + headerHtml + \"</div>\");\n    $(\"#footer\").html(\"<div class=\\\"content\\\"><a href=\\\"https://technitium.com/\\\" target=\\\"_blank\\\">Technitium</a> | <a href=\\\"https://blog.technitium.com/\\\" target=\\\"_blank\\\">Blog</a> | <a href=\\\"https://go.technitium.com/?id=35\\\" target=\\\"_blank\\\">Donate</a> | <a href=\\\"https://dnsclient.net/\\\" target=\\\"_blank\\\">DNS Client</a> | <a href=\\\"https://github.com/TechnitiumSoftware/DnsServer\\\" target=\\\"_blank\\\"><i class=\\\"fa fa-github\\\"></i>&nbsp;GitHub</a> | <a href=\\\"#\\\" onclick=\\\"showAbout(); return false;\\\">About</a></div>\");\n\n    loadQuickBlockLists();\n    loadQuickForwardersList();\n\n    $(\"#chkEnableUdpSocketPool\").on(\"click\", function () {\n        var enableUdpSocketPool = $(\"#chkEnableUdpSocketPool\").prop(\"checked\");\n\n        $(\"#txtUdpSocketPoolExcludedPorts\").prop(\"disabled\", !enableUdpSocketPool);\n    });\n\n    $(\"#chkEDnsClientSubnet\").on(\"click\", function () {\n        var eDnsClientSubnet = $(\"#chkEDnsClientSubnet\").prop(\"checked\");\n\n        $(\"#txtEDnsClientSubnetIPv4PrefixLength\").prop(\"disabled\", !eDnsClientSubnet);\n        $(\"#txtEDnsClientSubnetIPv6PrefixLength\").prop(\"disabled\", !eDnsClientSubnet);\n        $(\"#txtEDnsClientSubnetIpv4Override\").prop(\"disabled\", !eDnsClientSubnet);\n        $(\"#txtEDnsClientSubnetIpv6Override\").prop(\"disabled\", !eDnsClientSubnet);\n    });\n\n    $(\"#chkEnableBlocking\").on(\"click\", updateBlockingState);\n\n    $(\"input[type=radio][name=rdProxyType]\").on(\"change\", function () {\n        var proxyType = $(\"input[name=rdProxyType]:checked\").val().toLowerCase();\n        if (proxyType === \"none\") {\n            $(\"#txtProxyAddress\").prop(\"disabled\", true);\n            $(\"#txtProxyPort\").prop(\"disabled\", true);\n            $(\"#txtProxyUsername\").prop(\"disabled\", true);\n            $(\"#txtProxyPassword\").prop(\"disabled\", true);\n            $(\"#txtProxyBypassList\").prop(\"disabled\", true);\n        }\n        else {\n            $(\"#txtProxyAddress\").prop(\"disabled\", false);\n            $(\"#txtProxyPort\").prop(\"disabled\", false);\n            $(\"#txtProxyUsername\").prop(\"disabled\", false);\n            $(\"#txtProxyPassword\").prop(\"disabled\", false);\n            $(\"#txtProxyBypassList\").prop(\"disabled\", false);\n        }\n    });\n\n    $(\"input[type=radio][name=rdRecursion]\").on(\"change\", function () {\n        var recursion = $(\"input[name=rdRecursion]:checked\").val();\n\n        $(\"#txtRecursionNetworkACL\").prop(\"disabled\", recursion !== \"UseSpecifiedNetworkACL\");\n    });\n\n    $(\"input[type=radio][name=rdBlockingType]\").on(\"change\", function () {\n        var recursion = $(\"input[name=rdBlockingType]:checked\").val();\n        if (recursion === \"CustomAddress\") {\n            $(\"#txtCustomBlockingAddresses\").prop(\"disabled\", false);\n        }\n        else {\n            $(\"#txtCustomBlockingAddresses\").prop(\"disabled\", true);\n        }\n    });\n\n    $(\"#chkWebServiceEnableTls\").on(\"click\", function () {\n        var webServiceEnableTls = $(\"#chkWebServiceEnableTls\").prop(\"checked\");\n        $(\"#chkWebServiceEnableHttp3\").prop(\"disabled\", !webServiceEnableTls);\n        $(\"#chkWebServiceHttpToTlsRedirect\").prop(\"disabled\", !webServiceEnableTls);\n        $(\"#chkWebServiceUseSelfSignedTlsCertificate\").prop(\"disabled\", !webServiceEnableTls);\n        $(\"#txtWebServiceTlsPort\").prop(\"disabled\", !webServiceEnableTls);\n        $(\"#txtWebServiceTlsCertificatePath\").prop(\"disabled\", !webServiceEnableTls);\n        $(\"#txtWebServiceTlsCertificatePassword\").prop(\"disabled\", !webServiceEnableTls);\n    });\n\n    $(\"#chkEnableDnsOverUdpProxy\").on(\"click\", function () {\n        var enableDnsOverUdpProxy = $(\"#chkEnableDnsOverUdpProxy\").prop(\"checked\");\n        var enableDnsOverTcpProxy = $(\"#chkEnableDnsOverTcpProxy\").prop(\"checked\");\n        var enableDnsOverHttp = $(\"#chkEnableDnsOverHttp\").prop(\"checked\");\n        var enableDnsOverHttps = $(\"#chkEnableDnsOverHttps\").prop(\"checked\");\n\n        $(\"#txtDnsOverUdpProxyPort\").prop(\"disabled\", !enableDnsOverUdpProxy);\n        $(\"#txtReverseProxyNetworkACL\").prop(\"disabled\", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps);\n    });\n\n    $(\"#chkEnableDnsOverTcpProxy\").on(\"click\", function () {\n        var enableDnsOverUdpProxy = $(\"#chkEnableDnsOverUdpProxy\").prop(\"checked\");\n        var enableDnsOverTcpProxy = $(\"#chkEnableDnsOverTcpProxy\").prop(\"checked\");\n        var enableDnsOverHttp = $(\"#chkEnableDnsOverHttp\").prop(\"checked\");\n        var enableDnsOverHttps = $(\"#chkEnableDnsOverHttps\").prop(\"checked\");\n\n        $(\"#txtDnsOverTcpProxyPort\").prop(\"disabled\", !enableDnsOverTcpProxy);\n        $(\"#txtReverseProxyNetworkACL\").prop(\"disabled\", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps);\n    });\n\n    $(\"#chkEnableDnsOverHttp\").on(\"click\", function () {\n        var enableDnsOverUdpProxy = $(\"#chkEnableDnsOverUdpProxy\").prop(\"checked\");\n        var enableDnsOverTcpProxy = $(\"#chkEnableDnsOverTcpProxy\").prop(\"checked\");\n        var enableDnsOverHttp = $(\"#chkEnableDnsOverHttp\").prop(\"checked\");\n        var enableDnsOverHttps = $(\"#chkEnableDnsOverHttps\").prop(\"checked\");\n\n        $(\"#txtDnsOverHttpPort\").prop(\"disabled\", !enableDnsOverHttp);\n        $(\"#txtReverseProxyNetworkACL\").prop(\"disabled\", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps);\n        $(\"#txtDnsOverHttpRealIpHeader\").prop(\"disabled\", !enableDnsOverHttp && !enableDnsOverHttps);\n    });\n\n    $(\"#chkEnableDnsOverTls\").on(\"click\", function () {\n        var enableDnsOverTls = $(\"#chkEnableDnsOverTls\").prop(\"checked\");\n        var enableDnsOverHttps = $(\"#chkEnableDnsOverHttps\").prop(\"checked\");\n        var enableDnsOverQuic = $(\"#chkEnableDnsOverQuic\").prop(\"checked\");\n\n        $(\"#txtDnsOverTlsPort\").prop(\"disabled\", !enableDnsOverTls);\n        $(\"#txtDnsTlsCertificatePath\").prop(\"disabled\", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic);\n        $(\"#txtDnsTlsCertificatePassword\").prop(\"disabled\", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic);\n    });\n\n    $(\"#chkEnableDnsOverHttps\").on(\"click\", function () {\n        var enableDnsOverUdpProxy = $(\"#chkEnableDnsOverUdpProxy\").prop(\"checked\");\n        var enableDnsOverTcpProxy = $(\"#chkEnableDnsOverTcpProxy\").prop(\"checked\");\n        var enableDnsOverTls = $(\"#chkEnableDnsOverTls\").prop(\"checked\");\n        var enableDnsOverHttp = $(\"#chkEnableDnsOverHttp\").prop(\"checked\");\n        var enableDnsOverHttps = $(\"#chkEnableDnsOverHttps\").prop(\"checked\");\n        var enableDnsOverQuic = $(\"#chkEnableDnsOverQuic\").prop(\"checked\");\n\n        $(\"#chkEnableDnsOverHttp3\").prop(\"disabled\", !enableDnsOverHttps);\n        $(\"#txtDnsOverHttpsPort\").prop(\"disabled\", !enableDnsOverHttps);\n        $(\"#txtReverseProxyNetworkACL\").prop(\"disabled\", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps);\n        $(\"#txtDnsTlsCertificatePath\").prop(\"disabled\", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic);\n        $(\"#txtDnsTlsCertificatePassword\").prop(\"disabled\", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic);\n        $(\"#txtDnsOverHttpRealIpHeader\").prop(\"disabled\", !enableDnsOverHttp && !enableDnsOverHttps);\n    });\n\n    $(\"#chkEnableDnsOverQuic\").on(\"click\", function () {\n        var enableDnsOverTls = $(\"#chkEnableDnsOverTls\").prop(\"checked\");\n        var enableDnsOverHttps = $(\"#chkEnableDnsOverHttps\").prop(\"checked\");\n        var enableDnsOverQuic = $(\"#chkEnableDnsOverQuic\").prop(\"checked\");\n\n        $(\"#txtDnsOverQuicPort\").prop(\"disabled\", !enableDnsOverQuic);\n        $(\"#txtDnsTlsCertificatePath\").prop(\"disabled\", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic);\n        $(\"#txtDnsTlsCertificatePassword\").prop(\"disabled\", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic);\n    });\n\n    $(\"#chkEnableConcurrentForwarding\").on(\"click\", function () {\n        var concurrentForwarding = $(\"#chkEnableConcurrentForwarding\").prop(\"checked\");\n        $(\"#txtForwarderConcurrency\").prop(\"disabled\", !concurrentForwarding)\n    });\n\n    $(\"input[type=radio][name=rdLoggingType]\").on(\"change\", function () {\n        var rdLoggingType = $(\"input[name=rdLoggingType]:checked\").val();\n        var enableLogging = rdLoggingType.toLowerCase() != \"none\";\n\n        $(\"#chkIgnoreResolverLogs\").prop(\"disabled\", !enableLogging);\n        $(\"#chkLogQueries\").prop(\"disabled\", !enableLogging);\n        $(\"#chkUseLocalTime\").prop(\"disabled\", !enableLogging);\n        $(\"#txtLogFolderPath\").prop(\"disabled\", !enableLogging);\n    });\n\n    $(\"#chkServeStale\").on(\"click\", function () {\n        var serveStale = $(\"#chkServeStale\").prop(\"checked\");\n        $(\"#txtServeStaleTtl\").prop(\"disabled\", !serveStale);\n        $(\"#txtServeStaleAnswerTtl\").prop(\"disabled\", !serveStale);\n        $(\"#txtServeStaleResetTtl\").prop(\"disabled\", !serveStale);\n        $(\"#txtServeStaleMaxWaitTime\").prop(\"disabled\", !serveStale);\n    });\n\n    $(\"#optQuickBlockList\").on(\"change\", function () {\n        var selectedOption = $(\"#optQuickBlockList\").val();\n\n        switch (selectedOption) {\n            case \"blank\":\n                break;\n\n            case \"none\":\n                $(\"#txtBlockListUrls\").val(\"\");\n                break;\n\n            default:\n                for (var i = 0; i < quickBlockLists.length; i++) {\n                    if (quickBlockLists[i].name === selectedOption) {\n                        var existingList;\n\n                        if (selectedOption.toLowerCase() == \"default\")\n                            existingList = \"\";\n                        else\n                            existingList = $(\"#txtBlockListUrls\").val();\n\n                        var newList = existingList;\n\n                        for (var j = 0; j < quickBlockLists[i].urls.length; j++) {\n                            var url = quickBlockLists[i].urls[j];\n\n                            if (existingList.indexOf(url) < 0)\n                                newList += url + \"\\n\";\n                        }\n\n                        $(\"#txtBlockListUrls\").val(newList);\n                        break;\n                    }\n                }\n\n                break;\n        }\n    });\n\n    $(\"#optQuickForwarders\").on(\"change\", function () {\n        var selectedOption = $(\"#optQuickForwarders\").val();\n\n        switch (selectedOption) {\n            case \"blank\":\n                break;\n\n            case \"none\":\n                $(\"#txtForwarders\").val(\"\");\n                $(\"#rdForwarderProtocolUdp\").prop(\"checked\", true);\n                break;\n\n            default:\n                for (var i = 0; i < quickForwardersList.length; i++) {\n                    if (quickForwardersList[i].name === selectedOption) {\n                        var forwarders = \"\";\n\n                        for (var j = 0; j < quickForwardersList[i].addresses.length; j++) {\n                            forwarders += quickForwardersList[i].addresses[j] + \"\\n\";\n                        }\n\n                        $(\"#txtForwarders\").val(forwarders);\n\n                        switch (quickForwardersList[i].protocol.toUpperCase()) {\n                            case \"TCP\":\n                                $(\"#rdForwarderProtocolTcp\").prop(\"checked\", true);\n                                break;\n\n                            case \"TLS\":\n                                $(\"#rdForwarderProtocolTls\").prop(\"checked\", true);\n                                break;\n\n                            case \"HTTPS\":\n                                $(\"#rdForwarderProtocolHttps\").prop(\"checked\", true);\n                                break;\n\n                            case \"QUIC\":\n                                $(\"#rdForwarderProtocolQuic\").prop(\"checked\", true);\n                                break;\n\n                            default:\n                                $(\"#rdForwarderProtocolUdp\").prop(\"checked\", true);\n                                break;\n                        }\n\n                        if (quickForwardersList[i].proxyType == null)\n                            quickForwardersList[i].proxyType = \"DefaultProxy\";\n\n                        switch (quickForwardersList[i].proxyType.toUpperCase()) {\n                            case \"SOCKS5\":\n                            case \"HTTP\":\n                                if (quickForwardersList[i].proxyType.toUpperCase() == \"SOCKS5\")\n                                    $(\"#rdProxyTypeSocks5\").prop(\"checked\", true);\n                                else\n                                    $(\"#rdProxyTypeHttp\").prop(\"checked\", true);\n\n                                $(\"#txtProxyAddress\").val(quickForwardersList[i].proxyAddress);\n                                $(\"#txtProxyPort\").val(quickForwardersList[i].proxyPort);\n                                $(\"#txtProxyUsername\").val(quickForwardersList[i].proxyUsername);\n                                $(\"#txtProxyPassword\").val(quickForwardersList[i].proxyPassword);\n\n                                $(\"#txtProxyAddress\").prop(\"disabled\", false);\n                                $(\"#txtProxyPort\").prop(\"disabled\", false);\n                                $(\"#txtProxyUsername\").prop(\"disabled\", false);\n                                $(\"#txtProxyPassword\").prop(\"disabled\", false);\n                                break;\n\n                            case \"NONE\":\n                                $(\"#rdProxyTypeNone\").prop(\"checked\", true);\n\n                                $(\"#txtProxyAddress\").prop(\"disabled\", true);\n                                $(\"#txtProxyPort\").prop(\"disabled\", true);\n                                $(\"#txtProxyUsername\").prop(\"disabled\", true);\n                                $(\"#txtProxyPassword\").prop(\"disabled\", true);\n\n                                $(\"#txtProxyAddress\").val(\"\");\n                                $(\"#txtProxyPort\").val(\"\");\n                                $(\"#txtProxyUsername\").val(\"\");\n                                $(\"#txtProxyPassword\").val(\"\");\n                                break;\n                        }\n\n                        break;\n                    }\n                }\n\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdStatType]\").on(\"change\", function () {\n        var type = $(\"input[name=rdStatType]:checked\").val();\n        if (type === \"custom\") {\n            $(\"#divCustomDayWise\").show();\n\n            if ($(\"#dpCustomDayWiseStart\").val() === \"\") {\n                $(\"#dpCustomDayWiseStart\").trigger(\"focus\");\n                return;\n            }\n\n            if ($(\"#dpCustomDayWiseEnd\").val() === \"\") {\n                $(\"#dpCustomDayWiseEnd\").trigger(\"focus\");\n                return;\n            }\n\n            refreshDashboard();\n        }\n        else {\n            $(\"#divCustomDayWise\").hide();\n\n            refreshDashboard();\n        }\n    });\n\n    $(\"#btnCustomDayWise\").on(\"click\", function () {\n        refreshDashboard();\n    });\n\n    applyTheme();\n});\n\nfunction showAbout() {\n    if ($(\"#pageLogin\").is(\":visible\")) {\n        window.open(\"https://technitium.com/aboutus.html\", \"_blank\");\n    }\n    else {\n        $(\"#mainPanelTabListDashboard\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneDashboard\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListZones\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneZones\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListCachedZones\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneCachedZones\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListAllowedZones\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneAllowedZones\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListBlockedZones\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneBlockedZones\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListApps\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneApps\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListDnsClient\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneDnsClient\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListSettings\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneSettings\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListDhcp\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneDhcp\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListAdmin\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneAdmin\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListLogs\").removeClass(\"active\");\n        $(\"#mainPanelTabPaneLogs\").removeClass(\"active\");\n\n        $(\"#mainPanelTabListAbout\").addClass(\"active\");\n        $(\"#mainPanelTabPaneAbout\").addClass(\"active\");\n\n        setTimeout(function () {\n            window.scroll({\n                top: 0,\n                left: 0,\n                behavior: \"smooth\"\n            });\n        }, 500);\n    }\n}\n\nfunction checkForUpdate() {\n    HTTPRequest({\n        url: \"api/user/checkForUpdate?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            var lnkUpdateAvailable = $(\"#lnkUpdateAvailable\");\n\n            if (responseJSON.response.updateAvailable) {\n                $(\"#lblUpdateVersion\").text(responseJSON.response.updateVersion);\n                $(\"#lblCurrentVersion\").text(responseJSON.response.currentVersion);\n\n                if (responseJSON.response.updateTitle == null)\n                    responseJSON.response.updateTitle = \"New Update Available!\";\n\n                lnkUpdateAvailable.text(responseJSON.response.updateTitle);\n                $(\"#lblUpdateAvailableTitle\").text(responseJSON.response.updateTitle);\n\n                var lblUpdateMessage = $(\"#lblUpdateMessage\");\n                var lnkUpdateDownload = $(\"#lnkUpdateDownload\");\n                var lnkUpdateInstructions = $(\"#lnkUpdateInstructions\");\n                var lnkUpdateChangeLog = $(\"#lnkUpdateChangeLog\");\n\n                if (responseJSON.response.updateMessage == null) {\n                    lblUpdateMessage.hide();\n                }\n                else {\n                    lblUpdateMessage.text(responseJSON.response.updateMessage);\n                    lblUpdateMessage.show();\n                }\n\n                if (responseJSON.response.downloadLink == null) {\n                    lnkUpdateDownload.hide();\n                }\n                else {\n                    lnkUpdateDownload.attr(\"href\", responseJSON.response.downloadLink);\n                    lnkUpdateDownload.show();\n                }\n\n                if (responseJSON.response.instructionsLink == null) {\n                    lnkUpdateInstructions.hide();\n                }\n                else {\n                    lnkUpdateInstructions.attr(\"href\", responseJSON.response.instructionsLink);\n                    lnkUpdateInstructions.show();\n                }\n\n                if (responseJSON.response.changeLogLink == null) {\n                    lnkUpdateChangeLog.hide();\n                }\n                else {\n                    lnkUpdateChangeLog.attr(\"href\", responseJSON.response.changeLogLink);\n                    lnkUpdateChangeLog.show();\n                }\n\n                lnkUpdateAvailable.show();\n            }\n            else {\n                lnkUpdateAvailable.hide();\n            }\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction loadQuickBlockLists() {\n    $.ajax({\n        type: \"GET\",\n        url: \"json/quick-block-lists-custom.json\",\n        dataType: \"json\",\n        cache: false,\n        async: false,\n        success: function (responseJSON, status, jqXHR) {\n            loadQuickBlockListsFrom(responseJSON);\n        },\n        error: function (jqXHR, textStatus, errorThrown) {\n            $.ajax({\n                type: \"GET\",\n                url: \"json/quick-block-lists-builtin.json\",\n                dataType: \"json\",\n                cache: false,\n                async: false,\n                success: function (responseJSON, status, jqXHR) {\n                    loadQuickBlockListsFrom(responseJSON);\n                },\n                error: function (jqXHR, textStatus, errorThrown) {\n                    showAlert(\"danger\", \"Error!\", \"Failed to load Quick Forwarders list: \" + jqXHR.status + \" \" + jqXHR.statusText);\n                }\n            });\n        }\n    });\n}\n\nfunction loadQuickBlockListsFrom(responseJSON) {\n    var htmlList = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n    for (var i = 0; i < responseJSON.length; i++) {\n        htmlList += \"<option>\" + htmlEncode(responseJSON[i].name) + \"</option>\";\n    }\n\n    quickBlockLists = responseJSON;\n    $(\"#optQuickBlockList\").html(htmlList);\n}\n\nfunction loadQuickForwardersList() {\n    $.ajax({\n        type: \"GET\",\n        url: \"json/quick-forwarders-list-custom.json\",\n        dataType: \"json\",\n        cache: false,\n        async: false,\n        success: function (responseJSON, status, jqXHR) {\n            loadQuickForwardersListFrom(responseJSON);\n        },\n        error: function (jqXHR, textStatus, errorThrown) {\n            $.ajax({\n                type: \"GET\",\n                url: \"json/quick-forwarders-list-builtin.json\",\n                dataType: \"json\",\n                cache: false,\n                async: false,\n                success: function (responseJSON, status, jqXHR) {\n                    loadQuickForwardersListFrom(responseJSON);\n                },\n                error: function (jqXHR, textStatus, errorThrown) {\n                    showAlert(\"danger\", \"Error!\", \"Failed to load Quick Forwarders list: \" + jqXHR.status + \" \" + jqXHR.statusText);\n                }\n            });\n        }\n    });\n}\n\nfunction loadQuickForwardersListFrom(responseJSON) {\n    var htmlList = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n    for (var i = 0; i < responseJSON.length; i++) {\n        htmlList += \"<option>\" + htmlEncode(responseJSON[i].name) + \"</option>\";\n    }\n\n    quickForwardersList = responseJSON;\n    $(\"#optQuickForwarders\").html(htmlList);\n}\n\nfunction refreshDnsSettings() {\n    var divDnsSettingsLoader = $(\"#divDnsSettingsLoader\");\n    var divDnsSettings = $(\"#divDnsSettings\");\n\n    var node = $(\"#optSettingsClusterNode\").val();\n\n    divDnsSettings.hide();\n    divDnsSettingsLoader.show();\n\n    HTTPRequest({\n        url: \"api/settings/get?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            if ((node == \"\") || (node == \"cluster\") || (node == sessionData.info.dnsServerDomain))\n                updateDnsSettingsDataAndGui(responseJSON);\n\n            loadDnsSettings(responseJSON);\n            checkForReverseProxy(responseJSON);\n\n            if (node == \"cluster\") {\n                //cluster view\n                //general\n                $(\"#divSettingsGeneralLocalParameters\").hide();\n                $(\"#divSettingsGeneralDefaultParameters\").show();\n                $(\"#divSettingsGeneralDnsApps\").show();\n                $(\"#divSettingsGeneralIpv6\").hide();\n                $(\"#divSettingsGeneralUdpSocketPool\").hide();\n                $(\"#divSettingsGeneralEDns\").show();\n                $(\"#divSettingsGeneralDnssec\").show();\n                $(\"#divSettingsGeneralEDnsClientSubnet\").show();\n                $(\"#divSettingsGeneralRateLimiting\").show();\n                $(\"#divSettingsGeneralAdvancedOptions\").show();\n\n                //web service\n                $(\"#settingsTabListWebService\").hide();\n\n                if ($(\"#settingsTabListWebService\").hasClass(\"active\")) {\n                    $(\"#settingsTabListWebService\").removeClass(\"active\");\n                    $(\"#settingsTabPaneWebService\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //optional protocols\n                $(\"#settingsTabListOptionalProtocols\").hide();\n\n                if ($(\"#settingsTabListOptionalProtocols\").hasClass(\"active\")) {\n                    $(\"#settingsTabListOptionalProtocols\").removeClass(\"active\");\n                    $(\"#settingsTabPaneOptionalProtocols\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //tsig\n                $(\"#settingsTabListTsig\").show();\n\n                //recursion\n                $(\"#settingsTabListRecursion\").show();\n\n                //cache\n                $(\"#settingsTabListCache\").hide();\n\n                if ($(\"#settingsTabListCache\").hasClass(\"active\")) {\n                    $(\"#settingsTabListCache\").removeClass(\"active\");\n                    $(\"#settingsTabPaneCache\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //blocking\n                $(\"#settingsTabListBlocking\").show();\n\n                //proxy & forwarders\n                $(\"#settingsTabListProxyForwarders\").show();\n\n                //logging\n                $(\"#settingsTabListLogging\").hide();\n\n                if ($(\"#settingsTabListLogging\").hasClass(\"active\")) {\n                    $(\"#settingsTabListLogging\").removeClass(\"active\");\n                    $(\"#settingsTabPaneLogging\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //buttons\n                $(\"#btnSettingsFlushCache\").hide();\n                $(\"#btnShowBackupSettingsModal\").hide();\n                $(\"#btnShowRestoreSettingsModal\").hide();\n            }\n            else if (node != \"\") {\n                //node view\n                //general\n                $(\"#divSettingsGeneralLocalParameters\").show();\n                $(\"#divSettingsGeneralDefaultParameters\").hide();\n                $(\"#divSettingsGeneralDnsApps\").hide();\n                $(\"#divSettingsGeneralIpv6\").show();\n                $(\"#divSettingsGeneralUdpSocketPool\").show();\n                $(\"#divSettingsGeneralEDns\").hide();\n                $(\"#divSettingsGeneralDnssec\").hide();\n                $(\"#divSettingsGeneralEDnsClientSubnet\").hide();\n                $(\"#divSettingsGeneralRateLimiting\").hide();\n                $(\"#divSettingsGeneralAdvancedOptions\").hide();\n\n                //web service\n                $(\"#settingsTabListWebService\").show();\n\n                //optional protocols\n                $(\"#settingsTabListOptionalProtocols\").show();\n\n                //tsig\n                $(\"#settingsTabListTsig\").hide();\n\n                if ($(\"#settingsTabListTsig\").hasClass(\"active\")) {\n                    $(\"#settingsTabListTsig\").removeClass(\"active\");\n                    $(\"#settingsTabPaneTsig\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //recursion\n                $(\"#settingsTabListRecursion\").hide();\n\n                if ($(\"#settingsTabListRecursion\").hasClass(\"active\")) {\n                    $(\"#settingsTabListRecursion\").removeClass(\"active\");\n                    $(\"#settingsTabPaneRecursion\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //cache\n                $(\"#settingsTabListCache\").show();\n\n                //blocking\n                $(\"#settingsTabListBlocking\").hide();\n\n                if ($(\"#settingsTabListBlocking\").hasClass(\"active\")) {\n                    $(\"#settingsTabListBlocking\").removeClass(\"active\");\n                    $(\"#settingsTabPaneBlocking\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //proxy & forwarders\n                $(\"#settingsTabListProxyForwarders\").hide();\n\n                if ($(\"#settingsTabListProxyForwarders\").hasClass(\"active\")) {\n                    $(\"#settingsTabListProxyForwarders\").removeClass(\"active\");\n                    $(\"#settingsTabPaneProxyForwarders\").removeClass(\"active\");\n\n                    $(\"#settingsTabListGeneral\").addClass(\"active\");\n                    $(\"#settingsTabPaneGeneral\").addClass(\"active\");\n                }\n\n                //logging\n                $(\"#settingsTabListLogging\").show();\n\n                //buttons\n                $(\"#btnSettingsFlushCache\").show();\n                $(\"#btnShowBackupSettingsModal\").show();\n                $(\"#btnShowRestoreSettingsModal\").show();\n            }\n            else {\n                //clustering disabled\n                //general\n                $(\"#divSettingsGeneralLocalParameters\").show();\n                $(\"#divSettingsGeneralDefaultParameters\").show();\n                $(\"#divSettingsGeneralDnsApps\").show();\n                $(\"#divSettingsGeneralIpv6\").show();\n                $(\"#divSettingsGeneralUdpSocketPool\").show();\n                $(\"#divSettingsGeneralEDns\").show();\n                $(\"#divSettingsGeneralDnssec\").show();\n                $(\"#divSettingsGeneralEDnsClientSubnet\").show();\n                $(\"#divSettingsGeneralRateLimiting\").show();\n                $(\"#divSettingsGeneralAdvancedOptions\").show();\n\n                //web service\n                $(\"#settingsTabListWebService\").show();\n\n                //optional protocols\n                $(\"#settingsTabListOptionalProtocols\").show();\n\n                //tsig\n                $(\"#settingsTabListTsig\").show();\n\n                //recursion\n                $(\"#settingsTabListRecursion\").show();\n\n                //cache\n                $(\"#settingsTabListCache\").show();\n\n                //blocking\n                $(\"#settingsTabListBlocking\").show();\n\n                //proxy & forwarders\n                $(\"#settingsTabListProxyForwarders\").show();\n\n                //logging\n                $(\"#settingsTabListLogging\").show();\n\n                //buttons\n                $(\"#btnSettingsFlushCache\").show();\n                $(\"#btnShowBackupSettingsModal\").show();\n                $(\"#btnShowRestoreSettingsModal\").show();\n            }\n\n            divDnsSettingsLoader.hide();\n            divDnsSettings.show();\n        },\n        error: function () {\n            divDnsSettingsLoader.hide();\n            divDnsSettings.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDnsSettingsLoader\n    });\n}\n\nfunction getArrayAsString(array) {\n    var value = \"\";\n\n    for (var i = 0; i < array.length; i++)\n        value += array[i] + \"\\r\\n\";\n\n    return value;\n}\n\nfunction updateDnsSettingsDataAndGui(responseJSON) {\n    sessionData.info.dnsServerDomain = responseJSON.response.dnsServerDomain;\n    sessionData.info.uptimestamp = responseJSON.response.uptimestamp; //update timestamp since server may have restarted during current session\n\n    document.title = responseJSON.response.dnsServerDomain + \" - \" + \"Technitium DNS Server v\" + responseJSON.response.version;\n    $(\"#lblAboutVersion\").text(responseJSON.response.version);\n    $(\"#lblAboutUptime\").text(moment(responseJSON.response.uptimestamp).local().format(\"lll\") + \" (\" + moment(responseJSON.response.uptimestamp).fromNow() + \")\");\n    $(\"#lblDnsServerDomain\").text(\" - \" + responseJSON.response.dnsServerDomain);\n}\n\nfunction loadDnsSettings(responseJSON) {\n    //update cluster nodes\n    sessionData.info.clusterNodes = responseJSON.response.clusterNodes;\n    updateAllClusterNodeDropDowns();\n\n    if ($(\"#optSettingsClusterNode\").val() == \"cluster\")\n        updateClusterNodeDropDown($(\"#optSettingsClusterNode\"), true, \"cluster\");\n    else\n        updateClusterNodeDropDown($(\"#optSettingsClusterNode\"), true, responseJSON.response.dnsServerDomain);\n\n    //general\n    $(\"#txtDnsServerDomain\").val(responseJSON.response.dnsServerDomain);\n\n    var dnsServerLocalEndPoints = responseJSON.response.dnsServerLocalEndPoints;\n    if (dnsServerLocalEndPoints == null)\n        $(\"#txtDnsServerLocalEndPoints\").val(\"\");\n    else\n        $(\"#txtDnsServerLocalEndPoints\").val(getArrayAsString(dnsServerLocalEndPoints));\n\n    $(\"#txtDnsServerIPv4SourceAddresses\").val(getArrayAsString(responseJSON.response.dnsServerIPv4SourceAddresses));\n    $(\"#txtDnsServerIPv6SourceAddresses\").val(getArrayAsString(responseJSON.response.dnsServerIPv6SourceAddresses));\n\n    $(\"#txtDefaultRecordTtl\").val(responseJSON.response.defaultRecordTtl);\n    $(\"#txtDefaultNsRecordTtl\").val(responseJSON.response.defaultNsRecordTtl);\n    $(\"#txtDefaultSoaRecordTtl\").val(responseJSON.response.defaultSoaRecordTtl);\n\n    sessionData.info.defaultRecordTtl = responseJSON.response.defaultRecordTtl;\n    sessionData.info.defaultNsRecordTtl = responseJSON.response.defaultNsRecordTtl;\n    sessionData.info.defaultSoaRecordTtl = responseJSON.response.defaultSoaRecordTtl;\n\n    $(\"#txtDefaultResponsiblePerson\").val(responseJSON.response.defaultResponsiblePerson);\n    $(\"#chkUseSoaSerialDateScheme\").prop(\"checked\", responseJSON.response.useSoaSerialDateScheme);\n    $(\"#txtMinSoaRefresh\").val(responseJSON.response.minSoaRefresh);\n    $(\"#txtMinSoaRetry\").val(responseJSON.response.minSoaRetry);\n\n    $(\"#txtZoneTransferAllowedNetworks\").val(getArrayAsString(responseJSON.response.zoneTransferAllowedNetworks));\n    $(\"#txtNotifyAllowedNetworks\").val(getArrayAsString(responseJSON.response.notifyAllowedNetworks));\n\n    $(\"#chkDnsAppsEnableAutomaticUpdate\").prop(\"checked\", responseJSON.response.dnsAppsEnableAutomaticUpdate);\n\n    $(\"#chkPreferIPv6\").prop(\"checked\", responseJSON.response.preferIPv6);\n    $(\"#chkEnableUdpSocketPool\").prop(\"checked\", responseJSON.response.enableUdpSocketPool);\n    $(\"#txtUdpSocketPoolExcludedPorts\").prop(\"disabled\", !responseJSON.response.enableUdpSocketPool);\n    $(\"#txtUdpSocketPoolExcludedPorts\").val(getArrayAsString(responseJSON.response.socketPoolExcludedPorts));\n    $(\"#txtEdnsUdpPayloadSize\").val(responseJSON.response.udpPayloadSize);\n    $(\"#chkDnssecValidation\").prop(\"checked\", responseJSON.response.dnssecValidation);\n\n    $(\"#chkEDnsClientSubnet\").prop(\"checked\", responseJSON.response.eDnsClientSubnet);\n    $(\"#txtEDnsClientSubnetIPv4PrefixLength\").prop(\"disabled\", !responseJSON.response.eDnsClientSubnet);\n    $(\"#txtEDnsClientSubnetIPv6PrefixLength\").prop(\"disabled\", !responseJSON.response.eDnsClientSubnet);\n    $(\"#txtEDnsClientSubnetIpv4Override\").prop(\"disabled\", !responseJSON.response.eDnsClientSubnet);\n    $(\"#txtEDnsClientSubnetIpv6Override\").prop(\"disabled\", !responseJSON.response.eDnsClientSubnet);\n\n    $(\"#txtEDnsClientSubnetIPv4PrefixLength\").val(responseJSON.response.eDnsClientSubnetIPv4PrefixLength);\n    $(\"#txtEDnsClientSubnetIPv6PrefixLength\").val(responseJSON.response.eDnsClientSubnetIPv6PrefixLength);\n    $(\"#txtEDnsClientSubnetIpv4Override\").val(responseJSON.response.eDnsClientSubnetIpv4Override);\n    $(\"#txtEDnsClientSubnetIpv6Override\").val(responseJSON.response.eDnsClientSubnetIpv6Override);\n\n    $(\"#tableQpmPrefixLimitsIPv4\").html(\"\");\n\n    if (responseJSON.response.qpmPrefixLimitsIPv4 != null) {\n        for (var i = 0; i < responseJSON.response.qpmPrefixLimitsIPv4.length; i++) {\n            addQpmPrefixLimitsIPv4Row(responseJSON.response.qpmPrefixLimitsIPv4[i].prefix, responseJSON.response.qpmPrefixLimitsIPv4[i].udpLimit, responseJSON.response.qpmPrefixLimitsIPv4[i].tcpLimit);\n        }\n    }\n\n    $(\"#tableQpmPrefixLimitsIPv6\").html(\"\");\n\n    if (responseJSON.response.qpmPrefixLimitsIPv6 != null) {\n        for (var i = 0; i < responseJSON.response.qpmPrefixLimitsIPv6.length; i++) {\n            addQpmPrefixLimitsIPv6Row(responseJSON.response.qpmPrefixLimitsIPv6[i].prefix, responseJSON.response.qpmPrefixLimitsIPv6[i].udpLimit, responseJSON.response.qpmPrefixLimitsIPv6[i].tcpLimit);\n        }\n    }\n\n    $(\"#txtQpmLimitSampleMinutes\").val(responseJSON.response.qpmLimitSampleMinutes);\n    $(\"#txtQpmLimitUdpTruncation\").val(responseJSON.response.qpmLimitUdpTruncationPercentage);\n    $(\"#txtQpmLimitBypassList\").val(getArrayAsString(responseJSON.response.qpmLimitBypassList));\n\n    $(\"#txtClientTimeout\").val(responseJSON.response.clientTimeout);\n    $(\"#txtTcpSendTimeout\").val(responseJSON.response.tcpSendTimeout);\n    $(\"#txtTcpReceiveTimeout\").val(responseJSON.response.tcpReceiveTimeout);\n    $(\"#txtQuicIdleTimeout\").val(responseJSON.response.quicIdleTimeout);\n    $(\"#txtQuicMaxInboundStreams\").val(responseJSON.response.quicMaxInboundStreams);\n    $(\"#txtListenBacklog\").val(responseJSON.response.listenBacklog);\n    $(\"#txtMaxConcurrentResolutionsPerCore\").val(responseJSON.response.maxConcurrentResolutionsPerCore);\n\n    //web service\n    var webServiceLocalAddresses = responseJSON.response.webServiceLocalAddresses;\n    if (webServiceLocalAddresses == null)\n        $(\"#txtWebServiceLocalAddresses\").val(\"\");\n    else\n        $(\"#txtWebServiceLocalAddresses\").val(getArrayAsString(webServiceLocalAddresses));\n\n    $(\"#txtWebServiceHttpPort\").val(responseJSON.response.webServiceHttpPort);\n\n    $(\"#chkWebServiceEnableTls\").prop(\"checked\", responseJSON.response.webServiceEnableTls);\n\n    $(\"#chkWebServiceEnableHttp3\").prop(\"disabled\", !responseJSON.response.webServiceEnableTls);\n    $(\"#chkWebServiceHttpToTlsRedirect\").prop(\"disabled\", !responseJSON.response.webServiceEnableTls);\n    $(\"#chkWebServiceUseSelfSignedTlsCertificate\").prop(\"disabled\", !responseJSON.response.webServiceEnableTls);\n    $(\"#txtWebServiceTlsPort\").prop(\"disabled\", !responseJSON.response.webServiceEnableTls);\n    $(\"#txtWebServiceTlsCertificatePath\").prop(\"disabled\", !responseJSON.response.webServiceEnableTls);\n    $(\"#txtWebServiceTlsCertificatePassword\").prop(\"disabled\", !responseJSON.response.webServiceEnableTls);\n\n    $(\"#chkWebServiceEnableHttp3\").prop(\"checked\", responseJSON.response.webServiceEnableHttp3);\n    $(\"#chkWebServiceHttpToTlsRedirect\").prop(\"checked\", responseJSON.response.webServiceHttpToTlsRedirect);\n    $(\"#chkWebServiceUseSelfSignedTlsCertificate\").prop(\"checked\", responseJSON.response.webServiceUseSelfSignedTlsCertificate);\n    $(\"#txtWebServiceTlsPort\").val(responseJSON.response.webServiceTlsPort);\n    $(\"#txtWebServiceTlsCertificatePath\").val(responseJSON.response.webServiceTlsCertificatePath);\n\n    if (responseJSON.response.webServiceTlsCertificatePath == null)\n        $(\"#txtWebServiceTlsCertificatePassword\").val(\"\");\n    else\n        $(\"#txtWebServiceTlsCertificatePassword\").val(responseJSON.response.webServiceTlsCertificatePassword);\n\n    $(\"#txtWebServiceRealIpHeader\").val(responseJSON.response.webServiceRealIpHeader);\n    $(\"#lblWebServiceRealIpHeader\").text(responseJSON.response.webServiceRealIpHeader);\n    $(\"#lblWebServiceRealIpNginx\").text(\"proxy_set_header \" + responseJSON.response.webServiceRealIpHeader + \" $remote_addr;\");\n\n    //optional protocols\n    $(\"#chkEnableDnsOverUdpProxy\").prop(\"checked\", responseJSON.response.enableDnsOverUdpProxy);\n    $(\"#chkEnableDnsOverTcpProxy\").prop(\"checked\", responseJSON.response.enableDnsOverTcpProxy);\n    $(\"#chkEnableDnsOverHttp\").prop(\"checked\", responseJSON.response.enableDnsOverHttp);\n    $(\"#chkEnableDnsOverTls\").prop(\"checked\", responseJSON.response.enableDnsOverTls);\n    $(\"#chkEnableDnsOverHttps\").prop(\"checked\", responseJSON.response.enableDnsOverHttps);\n    $(\"#chkEnableDnsOverHttp3\").prop(\"disabled\", !responseJSON.response.enableDnsOverHttps);\n    $(\"#chkEnableDnsOverHttp3\").prop(\"checked\", responseJSON.response.enableDnsOverHttp3);\n    $(\"#chkEnableDnsOverQuic\").prop(\"checked\", responseJSON.response.enableDnsOverQuic);\n\n    $(\"#txtDnsOverUdpProxyPort\").prop(\"disabled\", !responseJSON.response.enableDnsOverUdpProxy);\n    $(\"#txtDnsOverTcpProxyPort\").prop(\"disabled\", !responseJSON.response.enableDnsOverTcpProxy);\n    $(\"#txtDnsOverHttpPort\").prop(\"disabled\", !responseJSON.response.enableDnsOverHttp);\n    $(\"#txtDnsOverTlsPort\").prop(\"disabled\", !responseJSON.response.enableDnsOverTls);\n    $(\"#txtDnsOverHttpsPort\").prop(\"disabled\", !responseJSON.response.enableDnsOverHttps);\n    $(\"#txtDnsOverQuicPort\").prop(\"disabled\", !responseJSON.response.enableDnsOverQuic);\n\n    $(\"#txtDnsOverUdpProxyPort\").val(responseJSON.response.dnsOverUdpProxyPort);\n    $(\"#txtDnsOverTcpProxyPort\").val(responseJSON.response.dnsOverTcpProxyPort);\n    $(\"#txtDnsOverHttpPort\").val(responseJSON.response.dnsOverHttpPort);\n    $(\"#txtDnsOverTlsPort\").val(responseJSON.response.dnsOverTlsPort);\n    $(\"#txtDnsOverHttpsPort\").val(responseJSON.response.dnsOverHttpsPort);\n    $(\"#txtDnsOverQuicPort\").val(responseJSON.response.dnsOverQuicPort);\n\n    $(\"#txtReverseProxyNetworkACL\").prop(\"disabled\", !responseJSON.response.enableDnsOverUdpProxy && !responseJSON.response.enableDnsOverTcpProxy && !responseJSON.response.enableDnsOverHttp && !responseJSON.response.enableDnsOverHttps);\n    $(\"#txtReverseProxyNetworkACL\").val(getArrayAsString(responseJSON.response.reverseProxyNetworkACL));\n\n    $(\"#txtDnsTlsCertificatePath\").prop(\"disabled\", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps && !responseJSON.response.enableDnsOverQuic);\n    $(\"#txtDnsTlsCertificatePassword\").prop(\"disabled\", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps && !responseJSON.response.enableDnsOverQuic);\n\n    $(\"#txtDnsTlsCertificatePath\").val(responseJSON.response.dnsTlsCertificatePath);\n\n    if (responseJSON.response.dnsTlsCertificatePath == null)\n        $(\"#txtDnsTlsCertificatePassword\").val(\"\");\n    else\n        $(\"#txtDnsTlsCertificatePassword\").val(responseJSON.response.dnsTlsCertificatePassword);\n\n    $(\"#lblDoHHost\").text(window.location.hostname + (responseJSON.response.dnsOverHttpPort == 80 ? \"\" : \":\" + responseJSON.response.dnsOverHttpPort));\n    $(\"#lblDoTHost\").text(\"tls-certificate-domain:\" + responseJSON.response.dnsOverTlsPort);\n    $(\"#lblDoQHost\").text(\"tls-certificate-domain:\" + responseJSON.response.dnsOverQuicPort);\n    $(\"#lblDoHsHost\").text(\"tls-certificate-domain\" + (responseJSON.response.dnsOverHttpsPort == 443 ? \"\" : \":\" + responseJSON.response.dnsOverHttpsPort));\n\n    $(\"#txtDnsOverHttpRealIpHeader\").prop(\"disabled\", !responseJSON.response.enableDnsOverHttp && !responseJSON.response.enableDnsOverHttps);\n    $(\"#txtDnsOverHttpRealIpHeader\").val(responseJSON.response.dnsOverHttpRealIpHeader);\n    $(\"#lblDnsOverHttpRealIpHeader\").text(responseJSON.response.dnsOverHttpRealIpHeader);\n    $(\"#lblDnsOverHttpRealIpNginx\").text(\"proxy_set_header \" + responseJSON.response.dnsOverHttpRealIpHeader + \" $remote_addr;\");\n\n    //tsig\n    $(\"#tableTsigKeys\").html(\"\");\n\n    if (responseJSON.response.tsigKeys != null) {\n        for (var i = 0; i < responseJSON.response.tsigKeys.length; i++) {\n            addTsigKeyRow(responseJSON.response.tsigKeys[i].keyName, responseJSON.response.tsigKeys[i].sharedSecret, responseJSON.response.tsigKeys[i].algorithmName);\n        }\n    }\n\n    //recursion\n    $(\"#txtRecursionNetworkACL\").prop(\"disabled\", true);\n\n    switch (responseJSON.response.recursion) {\n        case \"Allow\":\n            $(\"#rdRecursionAllow\").prop(\"checked\", true);\n            break;\n\n        case \"AllowOnlyForPrivateNetworks\":\n            $(\"#rdRecursionAllowOnlyForPrivateNetworks\").prop(\"checked\", true);\n            break;\n\n        case \"UseSpecifiedNetworkACL\":\n            $(\"#rdRecursionUseSpecifiedNetworkACL\").prop(\"checked\", true);\n            $(\"#txtRecursionNetworkACL\").prop(\"disabled\", false);\n            break;\n\n        case \"Deny\":\n        default:\n            $(\"#rdRecursionDeny\").prop(\"checked\", true);\n            break;\n    }\n\n    $(\"#txtRecursionNetworkACL\").val(getArrayAsString(responseJSON.response.recursionNetworkACL));\n\n    $(\"#chkRandomizeName\").prop(\"checked\", responseJSON.response.randomizeName);\n    $(\"#chkQnameMinimization\").prop(\"checked\", responseJSON.response.qnameMinimization);\n\n    $(\"#txtResolverRetries\").val(responseJSON.response.resolverRetries);\n    $(\"#txtResolverTimeout\").val(responseJSON.response.resolverTimeout);\n    $(\"#txtResolverConcurrency\").val(responseJSON.response.resolverConcurrency);\n    $(\"#txtResolverMaxStackCount\").val(responseJSON.response.resolverMaxStackCount);\n\n    //cache\n    $(\"#chkSaveCache\").prop(\"checked\", responseJSON.response.saveCache);\n\n    $(\"#chkServeStale\").prop(\"checked\", responseJSON.response.serveStale);\n\n    $(\"#txtServeStaleTtl\").prop(\"disabled\", !responseJSON.response.serveStale);\n    $(\"#txtServeStaleAnswerTtl\").prop(\"disabled\", !responseJSON.response.serveStale);\n    $(\"#txtServeStaleResetTtl\").prop(\"disabled\", !responseJSON.response.serveStale);\n    $(\"#txtServeStaleMaxWaitTime\").prop(\"disabled\", !responseJSON.response.serveStale);\n\n    $(\"#txtServeStaleTtl\").val(responseJSON.response.serveStaleTtl);\n    $(\"#txtServeStaleAnswerTtl\").val(responseJSON.response.serveStaleAnswerTtl);\n    $(\"#txtServeStaleResetTtl\").val(responseJSON.response.serveStaleResetTtl);\n    $(\"#txtServeStaleMaxWaitTime\").val(responseJSON.response.serveStaleMaxWaitTime);\n\n    $(\"#txtCacheMaximumEntries\").val(responseJSON.response.cacheMaximumEntries);\n    $(\"#txtCacheMinimumRecordTtl\").val(responseJSON.response.cacheMinimumRecordTtl);\n    $(\"#txtCacheMaximumRecordTtl\").val(responseJSON.response.cacheMaximumRecordTtl);\n    $(\"#txtCacheNegativeRecordTtl\").val(responseJSON.response.cacheNegativeRecordTtl);\n    $(\"#txtCacheFailureRecordTtl\").val(responseJSON.response.cacheFailureRecordTtl);\n\n    $(\"#txtCachePrefetchEligibility\").val(responseJSON.response.cachePrefetchEligibility);\n    $(\"#txtCachePrefetchTrigger\").val(responseJSON.response.cachePrefetchTrigger);\n    $(\"#txtCachePrefetchSampleIntervalInMinutes\").val(responseJSON.response.cachePrefetchSampleIntervalInMinutes);\n    $(\"#txtCachePrefetchSampleEligibilityHitsPerHour\").val(responseJSON.response.cachePrefetchSampleEligibilityHitsPerHour);\n\n    //blocking\n    $(\"#chkEnableBlocking\").prop(\"checked\", responseJSON.response.enableBlocking);\n\n    $(\"#chkAllowTxtBlockingReport\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#txtTemporaryDisableBlockingMinutes\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#btnTemporaryDisableBlockingNow\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#txtBlockingBypassList\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#rdBlockingTypeAnyAddress\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#rdBlockingTypeNxDomain\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#rdBlockingTypeCustomAddress\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#txtBlockListUrls\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#optQuickBlockList\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    $(\"#txtBlockListUpdateIntervalHours\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n\n    $(\"#chkAllowTxtBlockingReport\").prop(\"checked\", responseJSON.response.allowTxtBlockingReport);\n\n    if (responseJSON.response.temporaryDisableBlockingTill == null)\n        $(\"#lblTemporaryDisableBlockingTill\").text(\"Not Set\");\n    else\n        $(\"#lblTemporaryDisableBlockingTill\").text(moment(responseJSON.response.temporaryDisableBlockingTill).local().format(\"YYYY-MM-DD HH:mm:ss\"));\n\n    $(\"#txtTemporaryDisableBlockingMinutes\").val(\"\");\n\n    $(\"#txtCustomBlockingAddresses\").prop(\"disabled\", true);\n\n    $(\"#txtBlockingBypassList\").val(getArrayAsString(responseJSON.response.blockingBypassList));\n\n    switch (responseJSON.response.blockingType) {\n        case \"NxDomain\":\n            $(\"#rdBlockingTypeNxDomain\").prop(\"checked\", true);\n            break;\n\n        case \"CustomAddress\":\n            $(\"#rdBlockingTypeCustomAddress\").prop(\"checked\", true);\n            $(\"#txtCustomBlockingAddresses\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n            break;\n\n        case \"AnyAddress\":\n        default:\n            $(\"#rdBlockingTypeAnyAddress\").prop(\"checked\", true);\n            break;\n    }\n\n    $(\"#txtCustomBlockingAddresses\").val(getArrayAsString(responseJSON.response.customBlockingAddresses));\n\n    $(\"#txtBlockingAnswerTtl\").val(responseJSON.response.blockingAnswerTtl);\n\n    var blockListUrls = responseJSON.response.blockListUrls;\n    if (blockListUrls == null) {\n        $(\"#txtBlockListUrls\").val(\"\");\n        $(\"#btnUpdateBlockListsNow\").prop(\"disabled\", true);\n    }\n    else {\n        $(\"#txtBlockListUrls\").val(getArrayAsString(blockListUrls));\n        $(\"#btnUpdateBlockListsNow\").prop(\"disabled\", !responseJSON.response.enableBlocking);\n    }\n\n    $(\"#optQuickBlockList\").val(\"blank\");\n\n    $(\"#txtBlockListUpdateIntervalHours\").val(responseJSON.response.blockListUpdateIntervalHours);\n\n    if (responseJSON.response.blockListNextUpdatedOn == null) {\n        $(\"#lblBlockListNextUpdatedOn\").text(\"Not Scheduled\");\n    }\n    else {\n        var blockListNextUpdatedOn = moment(responseJSON.response.blockListNextUpdatedOn);\n\n        if (moment().utc().isBefore(blockListNextUpdatedOn))\n            $(\"#lblBlockListNextUpdatedOn\").text(blockListNextUpdatedOn.local().format(\"YYYY-MM-DD HH:mm:ss\"));\n        else\n            $(\"#lblBlockListNextUpdatedOn\").text(\"Updating Now\");\n    }\n\n    //proxy & forwarders\n    var proxy = responseJSON.response.proxy;\n    if (proxy === null) {\n        $(\"#rdProxyTypeNone\").prop(\"checked\", true);\n\n        $(\"#txtProxyAddress\").prop(\"disabled\", true);\n        $(\"#txtProxyPort\").prop(\"disabled\", true);\n        $(\"#txtProxyUsername\").prop(\"disabled\", true);\n        $(\"#txtProxyPassword\").prop(\"disabled\", true);\n        $(\"#txtProxyBypassList\").prop(\"disabled\", true);\n\n        $(\"#txtProxyAddress\").val(\"\");\n        $(\"#txtProxyPort\").val(\"\");\n        $(\"#txtProxyUsername\").val(\"\");\n        $(\"#txtProxyPassword\").val(\"\");\n        $(\"#txtProxyBypassList\").val(\"\");\n    }\n    else {\n        switch (proxy.type.toLowerCase()) {\n            case \"http\":\n                $(\"#rdProxyTypeHttp\").prop(\"checked\", true);\n                break;\n\n            case \"socks5\":\n                $(\"#rdProxyTypeSocks5\").prop(\"checked\", true);\n                break;\n\n            default:\n                $(\"#rdProxyTypeNone\").prop(\"checked\", true);\n                break;\n        }\n\n        $(\"#txtProxyAddress\").val(proxy.address);\n        $(\"#txtProxyPort\").val(proxy.port);\n        $(\"#txtProxyUsername\").val(proxy.username);\n        $(\"#txtProxyPassword\").val(proxy.password);\n        $(\"#txtProxyBypassList\").val(getArrayAsString(proxy.bypass));\n\n        $(\"#txtProxyAddress\").prop(\"disabled\", false);\n        $(\"#txtProxyPort\").prop(\"disabled\", false);\n        $(\"#txtProxyUsername\").prop(\"disabled\", false);\n        $(\"#txtProxyPassword\").prop(\"disabled\", false);\n        $(\"#txtProxyBypassList\").prop(\"disabled\", false);\n    }\n\n    var forwarders = responseJSON.response.forwarders;\n    if (forwarders == null)\n        $(\"#txtForwarders\").val(\"\");\n    else\n        $(\"#txtForwarders\").val(getArrayAsString(forwarders));\n\n    $(\"#optQuickForwarders\").val(\"blank\");\n\n    switch (responseJSON.response.forwarderProtocol.toLowerCase()) {\n        case \"tcp\":\n            $(\"#rdForwarderProtocolTcp\").prop(\"checked\", true);\n            break;\n\n        case \"tls\":\n            $(\"#rdForwarderProtocolTls\").prop(\"checked\", true);\n            break;\n\n        case \"https\":\n            $(\"#rdForwarderProtocolHttps\").prop(\"checked\", true);\n            break;\n\n        case \"quic\":\n            $(\"#rdForwarderProtocolQuic\").prop(\"checked\", true);\n            break;\n\n        default:\n            $(\"#rdForwarderProtocolUdp\").prop(\"checked\", true);\n            break;\n    }\n\n    $(\"#chkEnableConcurrentForwarding\").prop(\"checked\", responseJSON.response.concurrentForwarding);\n    $(\"#txtForwarderConcurrency\").prop(\"disabled\", !responseJSON.response.concurrentForwarding)\n\n    $(\"#txtForwarderRetries\").val(responseJSON.response.forwarderRetries);\n    $(\"#txtForwarderTimeout\").val(responseJSON.response.forwarderTimeout);\n    $(\"#txtForwarderConcurrency\").val(responseJSON.response.forwarderConcurrency);\n\n    //logging\n    var enableLogging;\n\n    switch (responseJSON.response.loggingType.toLowerCase()) {\n        case \"file\":\n            $(\"#rdLoggingTypeFile\").prop(\"checked\", true);\n            enableLogging = true;\n            break;\n\n        case \"console\":\n            $(\"#rdLoggingTypeConsole\").prop(\"checked\", true);\n            enableLogging = true;\n            break;\n\n        case \"fileandconsole\":\n            $(\"#rdLoggingTypeFileAndConsole\").prop(\"checked\", true);\n            enableLogging = true;\n            break;\n\n        default:\n            $(\"#rdLoggingTypeNone\").prop(\"checked\", true);\n            enableLogging = false;\n            break;\n    }\n\n    $(\"#chkIgnoreResolverLogs\").prop(\"disabled\", !enableLogging);\n    $(\"#chkLogQueries\").prop(\"disabled\", !enableLogging);\n    $(\"#chkUseLocalTime\").prop(\"disabled\", !enableLogging);\n    $(\"#txtLogFolderPath\").prop(\"disabled\", !enableLogging);\n\n    $(\"#chkIgnoreResolverLogs\").prop(\"checked\", responseJSON.response.ignoreResolverLogs);\n    $(\"#chkLogQueries\").prop(\"checked\", responseJSON.response.logQueries);\n    $(\"#chkUseLocalTime\").prop(\"checked\", responseJSON.response.useLocalTime);\n    $(\"#txtLogFolderPath\").val(responseJSON.response.logFolder);\n    $(\"#txtMaxLogFileDays\").val(responseJSON.response.maxLogFileDays);\n\n    $(\"#chkEnableInMemoryStats\").prop(\"checked\", responseJSON.response.enableInMemoryStats);\n    $(\"#txtMaxStatFileDays\").val(responseJSON.response.maxStatFileDays);\n}\n\nfunction saveDnsSettings(objBtn) {\n    var node = $(\"#optSettingsClusterNode\").val();\n\n    var includeClusterParameters = (node == \"\") || (node == \"cluster\");\n    var includeNodeParameters = (node == \"\") || !includeClusterParameters;\n\n    var formData = \"node=\" + encodeURIComponent(node);\n\n    //general\n    if (includeNodeParameters) {\n        var dnsServerDomain = $(\"#txtDnsServerDomain\").val();\n\n        if ((dnsServerDomain === null) || (dnsServerDomain === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter server domain name.\");\n            $(\"#txtDnsServerDomain\").trigger(\"focus\");\n            return;\n        }\n\n        var dnsServerLocalEndPoints = cleanTextList($(\"#txtDnsServerLocalEndPoints\").val());\n\n        if ((dnsServerLocalEndPoints.length === 0) || (dnsServerLocalEndPoints === \",\"))\n            dnsServerLocalEndPoints = \"0.0.0.0:53,[::]:53\";\n        else\n            $(\"#txtDnsServerLocalEndPoints\").val(dnsServerLocalEndPoints.replace(/,/g, \"\\n\"));\n\n        var dnsServerIPv4SourceAddresses = cleanTextList($(\"#txtDnsServerIPv4SourceAddresses\").val());\n        if ((dnsServerIPv4SourceAddresses.length == 0) || (dnsServerIPv4SourceAddresses === \",\"))\n            dnsServerIPv4SourceAddresses = false;\n\n        var dnsServerIPv6SourceAddresses = cleanTextList($(\"#txtDnsServerIPv6SourceAddresses\").val());\n        if ((dnsServerIPv6SourceAddresses.length == 0) || (dnsServerIPv6SourceAddresses === \",\"))\n            dnsServerIPv6SourceAddresses = false;\n\n        formData += \"&dnsServerDomain=\" + dnsServerDomain + \"&dnsServerLocalEndPoints=\" + encodeURIComponent(dnsServerLocalEndPoints) + \"&dnsServerIPv4SourceAddresses=\" + encodeURIComponent(dnsServerIPv4SourceAddresses) + \"&dnsServerIPv6SourceAddresses=\" + encodeURIComponent(dnsServerIPv6SourceAddresses)\n    }\n\n    if (includeClusterParameters) {\n        var defaultRecordTtl = $(\"#txtDefaultRecordTtl\").val();\n        var defaultNsRecordTtl = $(\"#txtDefaultNsRecordTtl\").val();\n        var defaultSoaRecordTtl = $(\"#txtDefaultSoaRecordTtl\").val();\n        var defaultResponsiblePerson = $(\"#txtDefaultResponsiblePerson\").val();\n        var useSoaSerialDateScheme = $(\"#chkUseSoaSerialDateScheme\").prop(\"checked\");\n        var minSoaRefresh = $(\"#txtMinSoaRefresh\").val();\n        var minSoaRetry = $(\"#txtMinSoaRetry\").val();\n\n        var zoneTransferAllowedNetworks = cleanTextList($(\"#txtZoneTransferAllowedNetworks\").val());\n        if ((zoneTransferAllowedNetworks.length == 0) || (zoneTransferAllowedNetworks === \",\"))\n            zoneTransferAllowedNetworks = false;\n        else\n            $(\"#txtZoneTransferAllowedNetworks\").val(zoneTransferAllowedNetworks.replace(/,/g, \"\\n\") + \"\\n\");\n\n        var notifyAllowedNetworks = cleanTextList($(\"#txtNotifyAllowedNetworks\").val());\n        if ((notifyAllowedNetworks.length == 0) || (notifyAllowedNetworks === \",\"))\n            notifyAllowedNetworks = false;\n        else\n            $(\"#txtNotifyAllowedNetworks\").val(notifyAllowedNetworks.replace(/,/g, \"\\n\") + \"\\n\");\n\n        var dnsAppsEnableAutomaticUpdate = $(\"#chkDnsAppsEnableAutomaticUpdate\").prop(\"checked\");\n\n        formData += \"&defaultRecordTtl=\" + encodeURIComponent(defaultRecordTtl) + \"&defaultNsRecordTtl=\" + encodeURIComponent(defaultNsRecordTtl) + \"&defaultSoaRecordTtl=\" + encodeURIComponent(defaultSoaRecordTtl) + \"&defaultResponsiblePerson=\" + encodeURIComponent(defaultResponsiblePerson) + \"&useSoaSerialDateScheme=\" + useSoaSerialDateScheme + \"&minSoaRefresh=\" + encodeURIComponent(minSoaRefresh) + \"&minSoaRetry=\" + encodeURIComponent(minSoaRetry) + \"&zoneTransferAllowedNetworks=\" + encodeURIComponent(zoneTransferAllowedNetworks) + \"&notifyAllowedNetworks=\" + encodeURIComponent(notifyAllowedNetworks) + \"&dnsAppsEnableAutomaticUpdate=\" + dnsAppsEnableAutomaticUpdate;\n    }\n\n    if (includeNodeParameters) {\n        var preferIPv6 = $(\"#chkPreferIPv6\").prop(\"checked\");\n        var enableUdpSocketPool = $(\"#chkEnableUdpSocketPool\").prop(\"checked\");\n\n        var socketPoolExcludedPorts = cleanTextList($(\"#txtUdpSocketPoolExcludedPorts\").val());\n        if ((socketPoolExcludedPorts.length == 0) || (socketPoolExcludedPorts === \",\"))\n            socketPoolExcludedPorts = false;\n        else\n            $(\"#txtUdpSocketPoolExcludedPorts\").val(socketPoolExcludedPorts.replace(/,/g, \"\\n\") + \"\\n\");\n\n        formData += \"&preferIPv6=\" + preferIPv6 + \"&enableUdpSocketPool=\" + enableUdpSocketPool + \"&socketPoolExcludedPorts=\" + encodeURIComponent(socketPoolExcludedPorts);\n    }\n\n    if (includeClusterParameters) {\n        var udpPayloadSize = $(\"#txtEdnsUdpPayloadSize\").val();\n        var dnssecValidation = $(\"#chkDnssecValidation\").prop(\"checked\");\n\n        var eDnsClientSubnet = $(\"#chkEDnsClientSubnet\").prop(\"checked\");\n\n        var eDnsClientSubnetIPv4PrefixLength = $(\"#txtEDnsClientSubnetIPv4PrefixLength\").val();\n        if ((eDnsClientSubnetIPv4PrefixLength == null) || (eDnsClientSubnetIPv4PrefixLength === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter EDNS Client Subnet IPv4 prefix length.\");\n            $(\"#txtEDnsClientSubnetIPv4PrefixLength\").trigger(\"focus\");\n            return;\n        }\n\n        var eDnsClientSubnetIPv6PrefixLength = $(\"#txtEDnsClientSubnetIPv6PrefixLength\").val();\n        if ((eDnsClientSubnetIPv6PrefixLength == null) || (eDnsClientSubnetIPv6PrefixLength === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter EDNS Client Subnet IPv6 prefix length.\");\n            $(\"#txtEDnsClientSubnetIPv6PrefixLength\").trigger(\"focus\");\n            return;\n        }\n\n        var eDnsClientSubnetIpv4Override = $(\"#txtEDnsClientSubnetIpv4Override\").val();\n        var eDnsClientSubnetIpv6Override = $(\"#txtEDnsClientSubnetIpv6Override\").val();\n\n        var qpmPrefixLimitsIPv4 = serializeTableData($(\"#tableQpmPrefixLimitsIPv4\"), 3);\n        if (qpmPrefixLimitsIPv4 === false)\n            return;\n\n        if (qpmPrefixLimitsIPv4.length === 0)\n            qpmPrefixLimitsIPv4 = false;\n\n        var qpmPrefixLimitsIPv6 = serializeTableData($(\"#tableQpmPrefixLimitsIPv6\"), 3);\n        if (qpmPrefixLimitsIPv6 === false)\n            return;\n\n        if (qpmPrefixLimitsIPv6.length === 0)\n            qpmPrefixLimitsIPv6 = false;\n\n        var qpmLimitSampleMinutes = $(\"#txtQpmLimitSampleMinutes\").val();\n        if ((qpmLimitSampleMinutes == null) || (qpmLimitSampleMinutes === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter Queries Per Minute (QPM) sample value.\");\n            $(\"#txtQpmLimitSampleMinutes\").trigger(\"focus\");\n            return;\n        }\n\n        var qpmLimitUdpTruncationPercentage = $(\"#txtQpmLimitUdpTruncation\").val();\n        if ((qpmLimitUdpTruncationPercentage == null) || (qpmLimitUdpTruncationPercentage === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter Queries Per Minute (QPM) limit UDP truncation percentage value.\");\n            $(\"#txtQpmLimitUdpTruncation\").trigger(\"focus\");\n            return;\n        }\n\n        var qpmLimitBypassList = cleanTextList($(\"#txtQpmLimitBypassList\").val());\n        if ((qpmLimitBypassList.length == 0) || (qpmLimitBypassList === \",\"))\n            qpmLimitBypassList = false;\n        else\n            $(\"#txtQpmLimitBypassList\").val(qpmLimitBypassList.replace(/,/g, \"\\n\") + \"\\n\");\n\n        var clientTimeout = $(\"#txtClientTimeout\").val();\n        if ((clientTimeout == null) || (clientTimeout === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Client Timeout.\");\n            $(\"#txtClientTimeout\").trigger(\"focus\");\n            return;\n        }\n\n        var tcpSendTimeout = $(\"#txtTcpSendTimeout\").val();\n        if ((tcpSendTimeout == null) || (tcpSendTimeout === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for TCP Send Timeout.\");\n            $(\"#txtTcpSendTimeout\").trigger(\"focus\");\n            return;\n        }\n\n        var tcpReceiveTimeout = $(\"#txtTcpReceiveTimeout\").val();\n        if ((tcpReceiveTimeout == null) || (tcpReceiveTimeout === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for TCP Receive Timeout.\");\n            $(\"#txtTcpReceiveTimeout\").trigger(\"focus\");\n            return;\n        }\n\n        var quicIdleTimeout = $(\"#txtQuicIdleTimeout\").val();\n        if ((quicIdleTimeout == null) || (quicIdleTimeout === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for QUIC Idle Timeout.\");\n            $(\"#txtQuicIdleTimeout\").trigger(\"focus\");\n            return;\n        }\n\n        var quicMaxInboundStreams = $(\"#txtQuicMaxInboundStreams\").val();\n        if ((quicMaxInboundStreams == null) || (quicMaxInboundStreams === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for QUIC Max Inbound Streams.\");\n            $(\"#txtQuicMaxInboundStreams\").trigger(\"focus\");\n            return;\n        }\n\n        var listenBacklog = $(\"#txtListenBacklog\").val();\n        if ((listenBacklog == null) || (listenBacklog === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Listen Backlog.\");\n            $(\"#txtListenBacklog\").trigger(\"focus\");\n            return;\n        }\n\n        var maxConcurrentResolutionsPerCore = $(\"#txtMaxConcurrentResolutionsPerCore\").val();\n        if ((maxConcurrentResolutionsPerCore == null) || (maxConcurrentResolutionsPerCore === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Max Concurrent Resolutions.\");\n            $(\"#txtMaxConcurrentResolutionsPerCore\").trigger(\"focus\");\n            return;\n        }\n\n        formData += \"&udpPayloadSize=\" + udpPayloadSize + \"&dnssecValidation=\" + dnssecValidation;\n        formData += \"&eDnsClientSubnet=\" + eDnsClientSubnet + \"&eDnsClientSubnetIPv4PrefixLength=\" + eDnsClientSubnetIPv4PrefixLength + \"&eDnsClientSubnetIPv6PrefixLength=\" + eDnsClientSubnetIPv6PrefixLength + \"&eDnsClientSubnetIpv4Override=\" + encodeURIComponent(eDnsClientSubnetIpv4Override) + \"&eDnsClientSubnetIpv6Override=\" + encodeURIComponent(eDnsClientSubnetIpv6Override);\n        formData += \"&qpmPrefixLimitsIPv4=\" + encodeURIComponent(qpmPrefixLimitsIPv4) + \"&qpmPrefixLimitsIPv6=\" + encodeURIComponent(qpmPrefixLimitsIPv6) + \"&qpmLimitSampleMinutes=\" + qpmLimitSampleMinutes + \"&qpmLimitUdpTruncationPercentage=\" + qpmLimitUdpTruncationPercentage + \"&qpmLimitBypassList=\" + encodeURIComponent(qpmLimitBypassList);\n        formData += \"&clientTimeout=\" + clientTimeout + \"&tcpSendTimeout=\" + tcpSendTimeout + \"&tcpReceiveTimeout=\" + tcpReceiveTimeout + \"&quicIdleTimeout=\" + quicIdleTimeout + \"&quicMaxInboundStreams=\" + quicMaxInboundStreams + \"&listenBacklog=\" + listenBacklog + \"&maxConcurrentResolutionsPerCore=\" + maxConcurrentResolutionsPerCore;\n    }\n\n    //web service\n    if (includeNodeParameters) {\n        var webServiceLocalAddresses = cleanTextList($(\"#txtWebServiceLocalAddresses\").val());\n\n        if ((webServiceLocalAddresses.length === 0) || (webServiceLocalAddresses === \",\"))\n            webServiceLocalAddresses = \"0.0.0.0,[::]\";\n        else\n            $(\"#txtWebServiceLocalAddresses\").val(webServiceLocalAddresses.replace(/,/g, \"\\n\"));\n\n        var webServiceHttpPort = $(\"#txtWebServiceHttpPort\").val();\n\n        if ((webServiceHttpPort === null) || (webServiceHttpPort === \"\"))\n            webServiceHttpPort = 5380;\n\n        var webServiceEnableTls = $(\"#chkWebServiceEnableTls\").prop(\"checked\");\n        var webServiceEnableHttp3 = $(\"#chkWebServiceEnableHttp3\").prop(\"checked\");\n        var webServiceHttpToTlsRedirect = $(\"#chkWebServiceHttpToTlsRedirect\").prop(\"checked\");\n        var webServiceUseSelfSignedTlsCertificate = $(\"#chkWebServiceUseSelfSignedTlsCertificate\").prop(\"checked\");\n        var webServiceTlsPort = $(\"#txtWebServiceTlsPort\").val();\n        var webServiceTlsCertificatePath = $(\"#txtWebServiceTlsCertificatePath\").val();\n        var webServiceTlsCertificatePassword = $(\"#txtWebServiceTlsCertificatePassword\").val();\n        var webServiceRealIpHeader = $(\"#txtWebServiceRealIpHeader\").val();\n\n        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);\n    }\n\n    //optional protocols\n    if (includeNodeParameters) {\n        var enableDnsOverUdpProxy = $(\"#chkEnableDnsOverUdpProxy\").prop(\"checked\");\n        var enableDnsOverTcpProxy = $(\"#chkEnableDnsOverTcpProxy\").prop(\"checked\");\n        var enableDnsOverHttp = $(\"#chkEnableDnsOverHttp\").prop(\"checked\");\n        var enableDnsOverTls = $(\"#chkEnableDnsOverTls\").prop(\"checked\");\n        var enableDnsOverHttps = $(\"#chkEnableDnsOverHttps\").prop(\"checked\");\n        var enableDnsOverHttp3 = $(\"#chkEnableDnsOverHttp3\").prop(\"checked\");\n        var enableDnsOverQuic = $(\"#chkEnableDnsOverQuic\").prop(\"checked\");\n\n        var dnsOverUdpProxyPort = $(\"#txtDnsOverUdpProxyPort\").val();\n        if ((dnsOverUdpProxyPort == null) || (dnsOverUdpProxyPort === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for DNS-over-UDP-PROXY Port.\");\n            $(\"#txtDnsOverUdpProxyPort\").trigger(\"focus\");\n            return;\n        }\n\n        var dnsOverTcpProxyPort = $(\"#txtDnsOverTcpProxyPort\").val();\n        if ((dnsOverTcpProxyPort == null) || (dnsOverTcpProxyPort === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for DNS-over-TCP-PROXY Port.\");\n            $(\"#txtDnsOverTcpProxyPort\").trigger(\"focus\");\n            return;\n        }\n\n        var dnsOverHttpPort = $(\"#txtDnsOverHttpPort\").val();\n        if ((dnsOverHttpPort == null) || (dnsOverHttpPort === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for DNS-over-HTTP Port.\");\n            $(\"#txtDnsOverHttpPort\").trigger(\"focus\");\n            return;\n        }\n\n        var dnsOverTlsPort = $(\"#txtDnsOverTlsPort\").val();\n        if ((dnsOverTlsPort == null) || (dnsOverTlsPort === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for DNS-over-TLS Port.\");\n            $(\"#txtDnsOverTlsPort\").trigger(\"focus\");\n            return;\n        }\n\n        var dnsOverHttpsPort = $(\"#txtDnsOverHttpsPort\").val();\n        if ((dnsOverHttpsPort == null) || (dnsOverHttpsPort === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for DNS-over-HTTPS Port.\");\n            $(\"#txtDnsOverHttpsPort\").trigger(\"focus\");\n            return;\n        }\n\n        var dnsOverQuicPort = $(\"#txtDnsOverQuicPort\").val();\n        if ((dnsOverQuicPort == null) || (dnsOverQuicPort === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for DNS-over-QUIC Port.\");\n            $(\"#txtDnsOverQuicPort\").trigger(\"focus\");\n            return;\n        }\n\n        var reverseProxyNetworkACL = cleanTextList($(\"#txtReverseProxyNetworkACL\").val());\n\n        if ((reverseProxyNetworkACL.length === 0) || (reverseProxyNetworkACL === \",\"))\n            reverseProxyNetworkACL = false;\n        else\n            $(\"#txtReverseProxyNetworkACL\").val(reverseProxyNetworkACL.replace(/,/g, \"\\n\"));\n\n        var dnsTlsCertificatePath = $(\"#txtDnsTlsCertificatePath\").val();\n        var dnsTlsCertificatePassword = $(\"#txtDnsTlsCertificatePassword\").val();\n\n        var dnsOverHttpRealIpHeader = $(\"#txtDnsOverHttpRealIpHeader\").val();\n\n        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);\n    }\n\n    //tsig\n    if (includeClusterParameters) {\n        var tsigKeys = serializeTableData($(\"#tableTsigKeys\"), 3);\n        if (tsigKeys === false)\n            return;\n\n        if (tsigKeys.length === 0)\n            tsigKeys = false;\n\n        formData += \"&tsigKeys=\" + encodeURIComponent(tsigKeys);\n    }\n\n    //recursion\n    if (includeClusterParameters) {\n        var recursion = $(\"input[name=rdRecursion]:checked\").val();\n\n        var recursionNetworkACL = cleanTextList($(\"#txtRecursionNetworkACL\").val());\n\n        if ((recursionNetworkACL.length === 0) || (recursionNetworkACL === \",\"))\n            recursionNetworkACL = false;\n        else\n            $(\"#txtRecursionNetworkACL\").val(recursionNetworkACL.replace(/,/g, \"\\n\"));\n\n        var randomizeName = $(\"#chkRandomizeName\").prop(\"checked\");\n        var qnameMinimization = $(\"#chkQnameMinimization\").prop(\"checked\");\n\n        var resolverRetries = $(\"#txtResolverRetries\").val();\n        if ((resolverRetries == null) || (resolverRetries === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Resolver Retries.\");\n            $(\"#txtResolverRetries\").trigger(\"focus\");\n            return;\n        }\n\n        var resolverTimeout = $(\"#txtResolverTimeout\").val();\n        if ((resolverTimeout == null) || (resolverTimeout === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Resolver Timeout.\");\n            $(\"#txtResolverTimeout\").trigger(\"focus\");\n            return;\n        }\n\n        var resolverConcurrency = $(\"#txtResolverConcurrency\").val();\n        if ((resolverConcurrency == null) || (resolverConcurrency === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Resolver Concurrency.\");\n            $(\"#txtResolverConcurrency\").trigger(\"focus\");\n            return;\n        }\n\n        var resolverMaxStackCount = $(\"#txtResolverMaxStackCount\").val();\n        if ((resolverMaxStackCount == null) || (resolverMaxStackCount === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Resolver Max Stack Count.\");\n            $(\"#txtResolverMaxStackCount\").trigger(\"focus\");\n            return;\n        }\n\n        formData += \"&recursion=\" + recursion + \"&recursionNetworkACL=\" + encodeURIComponent(recursionNetworkACL) + \"&randomizeName=\" + randomizeName + \"&qnameMinimization=\" + qnameMinimization + \"&resolverRetries=\" + resolverRetries + \"&resolverTimeout=\" + resolverTimeout + \"&resolverConcurrency=\" + resolverConcurrency + \"&resolverMaxStackCount=\" + resolverMaxStackCount;\n    }\n\n    //cache\n    if (includeNodeParameters) {\n        var saveCache = $(\"#chkSaveCache\").prop(\"checked\");\n\n        var serveStale = $(\"#chkServeStale\").prop(\"checked\");\n        var serveStaleTtl = $(\"#txtServeStaleTtl\").val();\n        var serveStaleAnswerTtl = $(\"#txtServeStaleAnswerTtl\").val();\n        var serveStaleResetTtl = $(\"#txtServeStaleResetTtl\").val();\n        var serveStaleMaxWaitTime = $(\"#txtServeStaleMaxWaitTime\").val();\n\n        var cacheMaximumEntries = $(\"#txtCacheMaximumEntries\").val();\n        if ((cacheMaximumEntries === null) || (cacheMaximumEntries === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache maximum entries value.\");\n            $(\"#txtCacheMaximumEntries\").trigger(\"focus\");\n            return;\n        }\n\n        var cacheMinimumRecordTtl = $(\"#txtCacheMinimumRecordTtl\").val();\n        if ((cacheMinimumRecordTtl === null) || (cacheMinimumRecordTtl === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache minimum record TTL value.\");\n            $(\"#txtCacheMinimumRecordTtl\").trigger(\"focus\");\n            return;\n        }\n\n        var cacheMaximumRecordTtl = $(\"#txtCacheMaximumRecordTtl\").val();\n        if ((cacheMaximumRecordTtl === null) || (cacheMaximumRecordTtl === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache maximum record TTL value.\");\n            $(\"#txtCacheMaximumRecordTtl\").trigger(\"focus\");\n            return;\n        }\n\n        var cacheNegativeRecordTtl = $(\"#txtCacheNegativeRecordTtl\").val();\n        if ((cacheNegativeRecordTtl === null) || (cacheNegativeRecordTtl === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache negative record TTL value.\");\n            $(\"#txtCacheNegativeRecordTtl\").trigger(\"focus\");\n            return;\n        }\n\n        var cacheFailureRecordTtl = $(\"#txtCacheFailureRecordTtl\").val();\n        if ((cacheFailureRecordTtl === null) || (cacheFailureRecordTtl === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache failure record TTL value.\");\n            $(\"#txtCacheFailureRecordTtl\").trigger(\"focus\");\n            return;\n        }\n\n        var cachePrefetchEligibility = $(\"#txtCachePrefetchEligibility\").val();\n        if ((cachePrefetchEligibility === null) || (cachePrefetchEligibility === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache prefetch eligibility value.\");\n            $(\"#txtCachePrefetchEligibility\").trigger(\"focus\");\n            return;\n        }\n\n        var cachePrefetchTrigger = $(\"#txtCachePrefetchTrigger\").val();\n        if ((cachePrefetchTrigger === null) || (cachePrefetchTrigger === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache prefetch trigger value.\");\n            $(\"#txtCachePrefetchTrigger\").trigger(\"focus\");\n            return;\n        }\n\n        var cachePrefetchSampleIntervalInMinutes = $(\"#txtCachePrefetchSampleIntervalInMinutes\").val();\n        if ((cachePrefetchSampleIntervalInMinutes === null) || (cachePrefetchSampleIntervalInMinutes === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache auto prefetch sample interval value.\");\n            $(\"#txtCachePrefetchSampleIntervalInMinutes\").trigger(\"focus\");\n            return;\n        }\n\n        var cachePrefetchSampleEligibilityHitsPerHour = $(\"#txtCachePrefetchSampleEligibilityHitsPerHour\").val();\n        if ((cachePrefetchSampleEligibilityHitsPerHour === null) || (cachePrefetchSampleEligibilityHitsPerHour === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter cache auto prefetch sample eligibility value.\");\n            $(\"#txtCachePrefetchSampleEligibilityHitsPerHour\").trigger(\"focus\");\n            return;\n        }\n\n        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;\n    }\n\n    //blocking\n    if (includeClusterParameters) {\n        var enableBlocking = $(\"#chkEnableBlocking\").prop(\"checked\");\n        var allowTxtBlockingReport = $(\"#chkAllowTxtBlockingReport\").prop(\"checked\");\n\n        var blockingBypassList = cleanTextList($(\"#txtBlockingBypassList\").val());\n        if ((blockingBypassList.length == 0) || (blockingBypassList === \",\"))\n            blockingBypassList = false;\n        else\n            $(\"#txtBlockingBypassList\").val(blockingBypassList.replace(/,/g, \"\\n\") + \"\\n\");\n\n        var blockingType = $(\"input[name=rdBlockingType]:checked\").val();\n\n        var customBlockingAddresses = cleanTextList($(\"#txtCustomBlockingAddresses\").val());\n        if ((customBlockingAddresses.length === 0) || customBlockingAddresses === \",\")\n            customBlockingAddresses = false;\n        else\n            $(\"#txtCustomBlockingAddresses\").val(customBlockingAddresses.replace(/,/g, \"\\n\") + \"\\n\");\n\n        var blockingAnswerTtl = $(\"#txtBlockingAnswerTtl\").val();\n\n        var blockListUrls = cleanTextList($(\"#txtBlockListUrls\").val());\n\n        if ((blockListUrls.length === 0) || (blockListUrls === \",\"))\n            blockListUrls = false;\n        else\n            $(\"#txtBlockListUrls\").val(blockListUrls.replace(/,/g, \"\\n\") + \"\\n\");\n\n        var blockListUpdateIntervalHours = $(\"#txtBlockListUpdateIntervalHours\").val();\n\n        formData += \"&enableBlocking=\" + enableBlocking + \"&allowTxtBlockingReport=\" + allowTxtBlockingReport + \"&blockingBypassList=\" + encodeURIComponent(blockingBypassList) + \"&blockingType=\" + blockingType + \"&customBlockingAddresses=\" + encodeURIComponent(customBlockingAddresses) + \"&blockingAnswerTtl=\" + blockingAnswerTtl + \"&blockListUrls=\" + encodeURIComponent(blockListUrls) + \"&blockListUpdateIntervalHours=\" + blockListUpdateIntervalHours;\n    }\n\n    //proxy & forwarders\n    if (includeClusterParameters) {\n        var proxy;\n\n        var proxyType = $(\"input[name=rdProxyType]:checked\").val().toLowerCase();\n        if (proxyType === \"none\") {\n            proxy = \"&proxyType=\" + proxyType;\n        }\n        else {\n            var proxyAddress = $(\"#txtProxyAddress\").val();\n\n            if ((proxyAddress === null) || (proxyAddress === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter proxy server address.\");\n                $(\"#txtProxyAddress\").trigger(\"focus\");\n                return;\n            }\n\n            var proxyPort = $(\"#txtProxyPort\").val();\n\n            if ((proxyPort === null) || (proxyPort === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter proxy server port.\");\n                $(\"#txtProxyPort\").trigger(\"focus\");\n                return;\n            }\n\n            var proxyBypass = cleanTextList($(\"#txtProxyBypassList\").val());\n\n            if ((proxyBypass.length === 0) || (proxyBypass === \",\"))\n                proxyBypass = \"\";\n            else\n                $(\"#txtProxyBypassList\").val(proxyBypass.replace(/,/g, \"\\n\"));\n\n            proxy = \"&proxyType=\" + proxyType + \"&proxyAddress=\" + encodeURIComponent(proxyAddress) + \"&proxyPort=\" + proxyPort + \"&proxyUsername=\" + encodeURIComponent($(\"#txtProxyUsername\").val()) + \"&proxyPassword=\" + encodeURIComponent($(\"#txtProxyPassword\").val()) + \"&proxyBypass=\" + encodeURIComponent(proxyBypass);\n        }\n\n        var forwarders = cleanTextList($(\"#txtForwarders\").val());\n\n        if ((forwarders.length === 0) || (forwarders === \",\"))\n            forwarders = false;\n        else\n            $(\"#txtForwarders\").val(forwarders.replace(/,/g, \"\\n\"));\n\n        var forwarderProtocol = $(\"input[name=rdForwarderProtocol]:checked\").val();\n\n        var concurrentForwarding = $(\"#chkEnableConcurrentForwarding\").prop(\"checked\");\n\n        var forwarderRetries = $(\"#txtForwarderRetries\").val();\n        if ((forwarderRetries == null) || (forwarderRetries === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Forwarder Retries.\");\n            $(\"#txtForwarderRetries\").trigger(\"focus\");\n            return;\n        }\n\n        var forwarderTimeout = $(\"#txtForwarderTimeout\").val();\n        if ((forwarderTimeout == null) || (forwarderTimeout === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Forwarder Timeout.\");\n            $(\"#txtForwarderTimeout\").trigger(\"focus\");\n            return;\n        }\n\n        var forwarderConcurrency = $(\"#txtForwarderConcurrency\").val();\n        if ((forwarderConcurrency == null) || (forwarderConcurrency === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a value for Forwarder Concurrency.\");\n            $(\"#txtForwarderConcurrency\").trigger(\"focus\");\n            return;\n        }\n\n        formData += proxy + \"&forwarders=\" + encodeURIComponent(forwarders) + \"&forwarderProtocol=\" + forwarderProtocol + \"&concurrentForwarding=\" + concurrentForwarding + \"&forwarderRetries=\" + forwarderRetries + \"&forwarderTimeout=\" + forwarderTimeout + \"&forwarderConcurrency=\" + forwarderConcurrency;\n    }\n\n    //logging\n    if (includeNodeParameters) {\n        var loggingType = $(\"input[name=rdLoggingType]:checked\").val();\n        var ignoreResolverLogs = $(\"#chkIgnoreResolverLogs\").prop(\"checked\");\n        var logQueries = $(\"#chkLogQueries\").prop(\"checked\");\n        var useLocalTime = $(\"#chkUseLocalTime\").prop(\"checked\");\n        var logFolder = $(\"#txtLogFolderPath\").val();\n        var maxLogFileDays = $(\"#txtMaxLogFileDays\").val();\n\n        var enableInMemoryStats = $(\"#chkEnableInMemoryStats\").prop(\"checked\");\n        var maxStatFileDays = $(\"#txtMaxStatFileDays\").val();\n\n        formData += \"&loggingType=\" + loggingType + \"&ignoreResolverLogs=\" + ignoreResolverLogs + \"&logQueries=\" + logQueries + \"&useLocalTime=\" + useLocalTime + \"&logFolder=\" + encodeURIComponent(logFolder) + \"&maxLogFileDays=\" + maxLogFileDays + \"&enableInMemoryStats=\" + enableInMemoryStats + \"&maxStatFileDays=\" + maxStatFileDays;\n    }\n\n    //send request\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/settings/set?token=\" + sessionData.token,\n        method: \"POST\",\n        data: formData,\n        processData: false,\n        showInnerError: true,\n        success: function (responseJSON) {\n            if ((node == \"\") || (node == sessionData.info.dnsServerDomain))\n                updateDnsSettingsDataAndGui(responseJSON);\n\n            loadDnsSettings(responseJSON);\n\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Settings Saved!\", \"DNS Server settings were saved successfully.\");\n\n            if (sessionData.info.dnsServerDomain == responseJSON.server)\n                checkForWebConsoleRedirection(responseJSON);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction addQpmPrefixLimitsIPv4Row(prefix, udpLimit, tcpLimit) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableQpmPrefixLimitsIPv4Row\" + id + \"\\\"><td><input type=\\\"number\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(prefix) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"number\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(udpLimit) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"number\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(tcpLimit) + \"\\\"></td>\";\n\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableQpmPrefixLimitsIPv4Row\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableQpmPrefixLimitsIPv4\").append(tableHtmlRows);\n}\n\nfunction addQpmPrefixLimitsIPv6Row(prefix, udpLimit, tcpLimit) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableQpmPrefixLimitsIPv6Row\" + id + \"\\\"><td><input type=\\\"number\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(prefix) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"number\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(udpLimit) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"number\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(tcpLimit) + \"\\\"></td>\";\n\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableQpmPrefixLimitsIPv6Row\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableQpmPrefixLimitsIPv6\").append(tableHtmlRows);\n}\n\nfunction addTsigKeyRow(keyName, sharedSecret, algorithmName) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableTsigKeyRow\" + id + \"\\\"><td><input type=\\\"text\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(keyName) + \"\\\"></td>\";\n    tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" data-optional=\\\"true\\\" value=\\\"\" + htmlEncode(sharedSecret) + \"\\\"></td>\";\n\n    tableHtmlRows += \"<td><select class=\\\"form-control\\\">\";\n    tableHtmlRows += \"<option value=\\\"hmac-md5.sig-alg.reg.int\\\"\" + (algorithmName == \"hmac-md5.sig-alg.reg.int\" ? \" selected\" : \"\") + \">HMAC-MD5 (obsolete)</option>\";\n    tableHtmlRows += \"<option value=\\\"hmac-sha1\\\"\" + (algorithmName == \"hmac-sha1\" ? \" selected\" : \"\") + \">HMAC-SHA1</option>\";\n    tableHtmlRows += \"<option value=\\\"hmac-sha256\\\"\" + (algorithmName == \"hmac-sha256\" ? \" selected\" : \"\") + \">HMAC-SHA256 (recommended)</option>\";\n    tableHtmlRows += \"<option value=\\\"hmac-sha256-128\\\"\" + (algorithmName == \"hmac-sha256-128\" ? \" selected\" : \"\") + \">HMAC-SHA256 (128 bits)</option>\";\n    tableHtmlRows += \"<option value=\\\"hmac-sha384\\\"\" + (algorithmName == \"hmac-sha384\" ? \" selected\" : \"\") + \">HMAC-SHA384</option>\";\n    tableHtmlRows += \"<option value=\\\"hmac-sha384-192\\\"\" + (algorithmName == \"hmac-sha384-192\" ? \" selected\" : \"\") + \">HMAC-SHA384 (192 bits)</option>\";\n    tableHtmlRows += \"<option value=\\\"hmac-sha512\\\"\" + (algorithmName == \"hmac-sha512\" ? \" selected\" : \"\") + \">HMAC-SHA512</option>\";\n    tableHtmlRows += \"<option value=\\\"hmac-sha512-256\\\"\" + (algorithmName == \"hmac-sha512-256\" ? \" selected\" : \"\") + \">HMAC-SHA512 (256 bits)</option>\";\n    tableHtmlRows += \"</select></td>\";\n\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"$('#tableTsigKeyRow\" + id + \"').remove();\\\">Delete</button></td></tr>\";\n\n    $(\"#tableTsigKeys\").append(tableHtmlRows);\n}\n\nfunction checkForReverseProxy(responseJSON) {\n    if (window.location.protocol == \"https:\") {\n        var currentPort = window.location.port;\n\n        if ((currentPort == 0) || (currentPort == \"\"))\n            currentPort = 443;\n\n        reverseProxyDetected = !responseJSON.response.webServiceEnableTls || (currentPort != responseJSON.response.webServiceTlsPort);\n    } else {\n        var currentPort = window.location.port;\n\n        if ((currentPort == 0) || (currentPort == \"\"))\n            currentPort = 80;\n\n        reverseProxyDetected = currentPort != responseJSON.response.webServiceHttpPort\n    }\n}\n\nfunction checkForWebConsoleRedirection(responseJSON) {\n    if (reverseProxyDetected)\n        return;\n\n    if (location.protocol == \"https:\") {\n        if (!responseJSON.response.webServiceEnableTls) {\n            setTimeout(function () {\n                window.open(\"http://\" + window.location.hostname + \":\" + responseJSON.response.webServiceHttpPort, \"_self\");\n            }, 2500); //delay redirection to allow web server to restart\n\n            return;\n        }\n\n        var currentPort = window.location.port;\n\n        if ((currentPort == 0) || (currentPort == \"\"))\n            currentPort = 443;\n\n        if (currentPort != responseJSON.response.webServiceTlsPort) {\n            setTimeout(function () {\n                window.open(\"https://\" + window.location.hostname + \":\" + responseJSON.response.webServiceTlsPort, \"_self\");\n            }, 2500); //delay redirection to allow web server to restart\n        }\n    }\n    else {\n        if (responseJSON.response.webServiceEnableTls && responseJSON.response.webServiceHttpToTlsRedirect) {\n            setTimeout(function () {\n                window.open(\"https://\" + window.location.hostname + \":\" + responseJSON.response.webServiceTlsPort, \"_self\");\n            }, 2500); //delay redirection to allow web server to restart\n\n            return;\n        }\n\n        var currentPort = window.location.port;\n\n        if ((currentPort == 0) || (currentPort == \"\"))\n            currentPort = 80;\n\n        if (currentPort != responseJSON.response.webServiceHttpPort) {\n            setTimeout(function () {\n                window.open(\"http://\" + window.location.hostname + \":\" + responseJSON.response.webServiceHttpPort, \"_self\");\n            }, 2500); //delay redirection to allow web server to restart\n        }\n    }\n}\n\nfunction forceUpdateBlockLists() {\n    if (!confirm(\"Are you sure to force download and update the block lists?\"))\n        return;\n\n    var btn = $(\"#btnUpdateBlockListsNow\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/settings/forceUpdateBlockLists?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n\n            $(\"#lblBlockListNextUpdatedOn\").text(\"Updating Now\");\n\n            showAlert(\"success\", \"Updating Block List!\", \"Block list update was triggered successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction temporaryDisableBlockingNow() {\n    var minutes = $(\"#txtTemporaryDisableBlockingMinutes\").val();\n\n    if ((minutes === null) || (minutes === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a value in minutes to temporarily disable blocking.\");\n        $(\"#txtTemporaryDisableBlockingMinutes\").trigger(\"focus\");\n        return;\n    }\n\n    if (!confirm(\"Are you sure to temporarily disable blocking for \" + minutes + \" minute(s)?\"))\n        return;\n\n    var btn = $(\"#btnTemporaryDisableBlockingNow\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/settings/temporaryDisableBlocking?token=\" + sessionData.token + \"&minutes=\" + minutes,\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n\n            $(\"#chkEnableBlocking\").prop(\"checked\", false);\n            $(\"#lblTemporaryDisableBlockingTill\").text(moment(responseJSON.response.temporaryDisableBlockingTill).local().format(\"YYYY-MM-DD HH:mm:ss\"));\n            updateBlockingState();\n\n            showAlert(\"success\", \"Blocking Disabled!\", \"Blocking was successfully disabled temporarily for \" + htmlEncode(minutes) + \" minute(s).\");\n\n            setTimeout(updateBlockingState, 500);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction updateBlockingState() {\n    var enableBlocking = $(\"#chkEnableBlocking\").prop(\"checked\");\n\n    $(\"#chkAllowTxtBlockingReport\").prop(\"disabled\", !enableBlocking);\n    $(\"#txtTemporaryDisableBlockingMinutes\").prop(\"disabled\", !enableBlocking);\n    $(\"#btnTemporaryDisableBlockingNow\").prop(\"disabled\", !enableBlocking);\n    $(\"#txtBlockingBypassList\").prop(\"disabled\", !enableBlocking);\n    $(\"#rdBlockingTypeAnyAddress\").prop(\"disabled\", !enableBlocking);\n    $(\"#rdBlockingTypeNxDomain\").prop(\"disabled\", !enableBlocking);\n    $(\"#rdBlockingTypeCustomAddress\").prop(\"disabled\", !enableBlocking);\n    $(\"#txtCustomBlockingAddresses\").prop(\"disabled\", !enableBlocking || !$(\"#rdBlockingTypeCustomAddress\").prop(\"checked\"));\n    $(\"#txtBlockListUrls\").prop(\"disabled\", !enableBlocking);\n    $(\"#optQuickBlockList\").prop(\"disabled\", !enableBlocking);\n    $(\"#txtBlockListUpdateIntervalHours\").prop(\"disabled\", !enableBlocking);\n    $(\"#btnUpdateBlockListsNow\").prop(\"disabled\", !enableBlocking || ($(\"#txtBlockListUrls\").val() == \"\"));\n}\n\nfunction updateChart(chart, data) {\n    chart.data = data;\n    chart.update();\n    loadChartLegendSettings(chart); //Reload the chart legend\n}\n\nfunction loadChartLegendSettings(chart) {\n    var labelFilters = localStorage.getItem(\"chart_\" + chart.id + \"_legend\");\n\n    if (labelFilters != null) {\n        labelFilters = JSON.parse(labelFilters);\n        if (chart.config.type == \"doughnut\" || chart.config.type == \"pie\") {\n            chart.data.labels.forEach((label, index) => {\n                let labelFilter = labelFilters.filter(function (f) {\n                    return f.title == this.toString();\n                }, label);\n                if (labelFilter.length > 0) {\n                    chart.getDatasetMeta(0).data[index].hidden = labelFilter[0].hidden;\n                }\n            });\n        }\n        else {\n            chart.data.datasets.forEach((data, index) => {\n                let labelFilter = labelFilters.filter(function (f) {\n                    return f.title == this.toString();\n                }, data.label);\n                if (labelFilter.length > 0) {\n                    chart.getDatasetMeta(index).hidden = labelFilter[0].hidden;\n                }\n            });\n        }\n\n        chart.update();\n    }\n}\n\nfunction saveChartLegendSettings(chart) {\n    var labelFilters = [];\n\n    if (chart.config.type == \"doughnut\" || chart.config.type == \"pie\") {\n        chart.data.labels.forEach((label, index) => {\n            var hidden = chart.getDatasetMeta(0).data[index].hidden;\n            labelFilters.push(\n                {\n                    title: label,\n                    hidden: hidden\n                }\n            );\n        });\n    }\n    else {\n        chart.data.datasets.forEach((data, index) => {\n            var hidden = chart.getDatasetMeta(index).hidden;\n            labelFilters.push(\n                {\n                    title: data.label,\n                    hidden: hidden\n                }\n            );\n        });\n    }\n\n    localStorage.setItem(\"chart_\" + chart.id + \"_legend\", JSON.stringify(labelFilters));\n}\n\nvar chartLegendOnClick = function (e, legendItem) {\n    var chartType = this.chart.config.type;\n\n    if (chartType == \"doughnut\") {\n        Chart.defaults.doughnut.legend.onClick.call(this, e, legendItem);\n    } else if (chartType == \"pie\") {\n        Chart.defaults.pie.legend.onClick.call(this, e, legendItem);\n    } else {\n        Chart.defaults.global.legend.onClick.call(this, e, legendItem);\n    }\n\n    saveChartLegendSettings(this.chart);\n}\n\nfunction refreshDashboard(hideLoader) {\n    if (!$(\"#mainPanelTabPaneDashboard\").hasClass(\"active\"))\n        return;\n\n    if (hideLoader == null)\n        hideLoader = false;\n\n    var divDashboardLoader = $(\"#divDashboardLoader\");\n    var divDashboard = $(\"#divDashboard\");\n\n    var type = $(\"input[name=rdStatType]:checked\").val();\n    var custom = \"\";\n\n    if (type === \"custom\") {\n        var txtStart = $(\"#dpCustomDayWiseStart\").val();\n        if (txtStart === null || (txtStart === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please select a start date.\");\n            $(\"#dpCustomDayWiseStart\").trigger(\"focus\");\n            return;\n        }\n\n        var txtEnd = $(\"#dpCustomDayWiseEnd\").val();\n        if (txtEnd === null || (txtEnd === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please select an end date.\");\n            $(\"#dpCustomDayWiseEnd\").trigger(\"focus\");\n            return;\n        }\n\n        var start = moment(txtStart);\n        var end = moment(txtEnd);\n\n        if ((end.diff(start, \"days\") + 1) > 7) {\n            start = moment.utc(txtStart).toISOString();\n            end = moment.utc(txtEnd).toISOString();\n        }\n        else {\n            start = start.toISOString();\n            end = end.toISOString();\n        }\n\n        custom = \"&start=\" + encodeURIComponent(start) + \"&end=\" + encodeURIComponent(end);\n    }\n\n    var node = $(\"#optDashboardClusterNode\").val();\n\n    if (!hideLoader) {\n        divDashboard.hide();\n        divDashboardLoader.show();\n    }\n\n    HTTPRequest({\n        url: \"api/dashboard/stats/get?token=\" + sessionData.token + \"&type=\" + type + \"&utc=true\" + custom + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n\n            //stats\n            $(\"#divDashboardStatsTotalQueries\").text(responseJSON.response.stats.totalQueries.toLocaleString());\n            $(\"#divDashboardStatsTotalNoError\").text(responseJSON.response.stats.totalNoError.toLocaleString());\n            $(\"#divDashboardStatsTotalServerFailure\").text(responseJSON.response.stats.totalServerFailure.toLocaleString());\n            $(\"#divDashboardStatsTotalNxDomain\").text(responseJSON.response.stats.totalNxDomain.toLocaleString());\n            $(\"#divDashboardStatsTotalRefused\").text(responseJSON.response.stats.totalRefused.toLocaleString());\n\n            $(\"#divDashboardStatsTotalAuthHit\").text(responseJSON.response.stats.totalAuthoritative.toLocaleString());\n            $(\"#divDashboardStatsTotalRecursions\").text(responseJSON.response.stats.totalRecursive.toLocaleString());\n            $(\"#divDashboardStatsTotalCacheHit\").text(responseJSON.response.stats.totalCached.toLocaleString());\n            $(\"#divDashboardStatsTotalBlocked\").text(responseJSON.response.stats.totalBlocked.toLocaleString());\n            $(\"#divDashboardStatsTotalDropped\").text(responseJSON.response.stats.totalDropped.toLocaleString());\n\n            $(\"#divDashboardStatsTotalClients\").text(responseJSON.response.stats.totalClients.toLocaleString());\n\n            $(\"#divDashboardStatsZones\").text(responseJSON.response.stats.zones.toLocaleString());\n            $(\"#divDashboardStatsCachedEntries\").text(responseJSON.response.stats.cachedEntries.toLocaleString());\n            $(\"#divDashboardStatsAllowedZones\").text(responseJSON.response.stats.allowedZones.toLocaleString());\n            $(\"#divDashboardStatsBlockedZones\").text(responseJSON.response.stats.blockedZones.toLocaleString());\n            $(\"#divDashboardStatsAllowListZones\").text(responseJSON.response.stats.allowListZones.toLocaleString());\n            $(\"#divDashboardStatsBlockListZones\").text(responseJSON.response.stats.blockListZones.toLocaleString());\n\n            if (responseJSON.response.stats.totalQueries > 0) {\n                $(\"#divDashboardStatsTotalNoErrorPercentage\").text((responseJSON.response.stats.totalNoError * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n                $(\"#divDashboardStatsTotalServerFailurePercentage\").text((responseJSON.response.stats.totalServerFailure * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n                $(\"#divDashboardStatsTotalNxDomainPercentage\").text((responseJSON.response.stats.totalNxDomain * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n                $(\"#divDashboardStatsTotalRefusedPercentage\").text((responseJSON.response.stats.totalRefused * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n\n                $(\"#divDashboardStatsTotalAuthHitPercentage\").text((responseJSON.response.stats.totalAuthoritative * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n                $(\"#divDashboardStatsTotalRecursionsPercentage\").text((responseJSON.response.stats.totalRecursive * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n                $(\"#divDashboardStatsTotalCacheHitPercentage\").text((responseJSON.response.stats.totalCached * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n                $(\"#divDashboardStatsTotalBlockedPercentage\").text((responseJSON.response.stats.totalBlocked * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n                $(\"#divDashboardStatsTotalDroppedPercentage\").text((responseJSON.response.stats.totalDropped * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + \"%\");\n            }\n            else {\n                $(\"#divDashboardStatsTotalNoErrorPercentage\").text(\"0%\");\n                $(\"#divDashboardStatsTotalServerFailurePercentage\").text(\"0%\");\n                $(\"#divDashboardStatsTotalNxDomainPercentage\").text(\"0%\");\n                $(\"#divDashboardStatsTotalRefusedPercentage\").text(\"0%\");\n\n                $(\"#divDashboardStatsTotalAuthHitPercentage\").text(\"0%\");\n                $(\"#divDashboardStatsTotalRecursionsPercentage\").text(\"0%\");\n                $(\"#divDashboardStatsTotalCacheHitPercentage\").text(\"0%\");\n                $(\"#divDashboardStatsTotalBlockedPercentage\").text(\"0%\");\n                $(\"#divDashboardStatsTotalDroppedPercentage\").text(\"0%\");\n            }\n\n            //main chart\n\n            //fix labels\n            switch (responseJSON.response.mainChartData.labelFormat) {\n                case \"MM/DD\":\n                case \"DD/MM\":\n                case \"MM/YYYY\":\n                    for (var i = 0; i < responseJSON.response.mainChartData.labels.length; i++) {\n                        responseJSON.response.mainChartData.labels[i] = moment(responseJSON.response.mainChartData.labels[i]).utc().format(responseJSON.response.mainChartData.labelFormat);\n                    }\n                    break;\n\n                default:\n                    for (var i = 0; i < responseJSON.response.mainChartData.labels.length; i++) {\n                        responseJSON.response.mainChartData.labels[i] = moment(responseJSON.response.mainChartData.labels[i]).local().format(responseJSON.response.mainChartData.labelFormat);\n                    }\n                    break;\n            }\n\n            if (window.chartDashboardMain == null) {\n                var contextDashboardMain = document.getElementById(\"canvasDashboardMain\").getContext('2d');\n\n                window.chartDashboardMain = new Chart(contextDashboardMain, {\n                    type: 'line',\n                    data: responseJSON.response.mainChartData,\n                    options: {\n                        elements: {\n                            line: {\n                                tension: 0.2,\n                            }\n                        },\n                        scales: {\n                            yAxes: [{\n                                ticks: {\n                                    beginAtZero: true\n                                }\n                            }]\n                        },\n                        legend: {\n                            onClick: chartLegendOnClick\n                        }\n                    }\n                });\n\n                loadChartLegendSettings(window.chartDashboardMain);\n            }\n            else {\n                updateChart(window.chartDashboardMain, responseJSON.response.mainChartData);\n            }\n\n            //query response chart\n            if (window.chartDashboardPie == null) {\n                var contextDashboardPie = document.getElementById(\"canvasDashboardPie\").getContext('2d');\n\n                window.chartDashboardPie = new Chart(contextDashboardPie, {\n                    type: 'doughnut',\n                    data: responseJSON.response.queryResponseChartData,\n                    options: {\n                        legend: {\n                            onClick: chartLegendOnClick\n                        }\n                    }\n                });\n\n                loadChartLegendSettings(window.chartDashboardPie);\n            }\n            else {\n                updateChart(window.chartDashboardPie, responseJSON.response.queryResponseChartData);\n            }\n\n            //query type chart\n            if (window.chartDashboardPie2 == null) {\n                var contextDashboardPie2 = document.getElementById(\"canvasDashboardPie2\").getContext('2d');\n\n                window.chartDashboardPie2 = new Chart(contextDashboardPie2, {\n                    type: 'doughnut',\n                    data: responseJSON.response.queryTypeChartData,\n                    options: {\n                        legend: {\n                            onClick: chartLegendOnClick\n                        }\n                    }\n                });\n\n                loadChartLegendSettings(window.chartDashboardPie2);\n            }\n            else {\n                updateChart(window.chartDashboardPie2, responseJSON.response.queryTypeChartData);\n            }\n\n            //protocol type chart\n            if (window.chartDashboardPie3 == null) {\n                var contextDashboardPie3 = document.getElementById(\"canvasDashboardPie3\").getContext('2d');\n\n                window.chartDashboardPie3 = new Chart(contextDashboardPie3, {\n                    type: 'doughnut',\n                    data: responseJSON.response.protocolTypeChartData,\n                    options: {\n                        legend: {\n                            onClick: chartLegendOnClick\n                        }\n                    }\n                });\n\n                loadChartLegendSettings(window.chartDashboardPie3);\n            }\n            else {\n                updateChart(window.chartDashboardPie3, responseJSON.response.protocolTypeChartData);\n            }\n\n            //top clients\n            {\n                var tableHtmlRows;\n                var topClients = responseJSON.response.topClients;\n\n                if (topClients.length < 1) {\n                    tableHtmlRows = \"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Data</td></tr>\";\n                }\n                else {\n                    tableHtmlRows = \"\";\n\n                    for (var i = 0; i < topClients.length; i++) {\n                        tableHtmlRows += \"<tr\" + (topClients[i].rateLimited ? \" style=\\\"color: orange;\\\"\" : \"\") + \"><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topClients[i].name) + (topClients[i].rateLimited ? \" (rate limited)\" : \"\") + \"<br />\" + htmlEncode(topClients[i].domain == \"\" ? \".\" : topClients[i].domain) + \"</td><td>\" + topClients[i].hits.toLocaleString();\n                        tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDashboardTopClientsRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"showQueryLogs(null, '\" + topClients[i].name + \"', '\" + node + \"'); return false;\\\">Show Query Logs</a></li>\";\n                        tableHtmlRows += \"</ul></div></td></tr>\";\n                    }\n                }\n\n                $(\"#tableTopClients\").html(tableHtmlRows);\n            }\n\n            //top domains\n            {\n                var tableHtmlRows;\n                var topDomains = responseJSON.response.topDomains;\n\n                if (topDomains.length < 1) {\n                    tableHtmlRows = \"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Data</td></tr>\";\n                }\n                else {\n                    tableHtmlRows = \"\";\n\n                    for (var i = 0; i < topDomains.length; i++) {\n                        if (topDomains[i].nameIdn == null)\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topDomains[i].name == \"\" ? \".\" : topDomains[i].name) + \"</td><td>\" + topDomains[i].hits.toLocaleString();\n                        else\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topDomains[i].nameIdn) + \"</td><td>\" + topDomains[i].hits.toLocaleString();\n\n                        tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDashboardTopDomainsRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"showQueryLogs('\" + topDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Show Query Logs</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"queryDnsServer('\" + topDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Query DNS Server</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-domain=\\\"\" + htmlEncode(topDomains[i].name) + \"\\\" onclick=\\\"blockDomain(this, 'btnDashboardTopDomainsRowOption'); return false;\\\">Block Domain</a></li>\";\n                        tableHtmlRows += \"</ul></div></td></tr>\";\n                    }\n                }\n\n                $(\"#tableTopDomains\").html(tableHtmlRows);\n            }\n\n            //top blocked domains\n            {\n                var tableHtmlRows;\n                var topBlockedDomains = responseJSON.response.topBlockedDomains;\n\n                if (topBlockedDomains.length < 1) {\n                    tableHtmlRows = \"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Data</td></tr>\";\n                }\n                else {\n                    tableHtmlRows = \"\";\n\n                    for (var i = 0; i < topBlockedDomains.length; i++) {\n                        if (topBlockedDomains[i].nameIdn == null)\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topBlockedDomains[i].name == \"\" ? \".\" : topBlockedDomains[i].name) + \"</td><td>\" + topBlockedDomains[i].hits.toLocaleString();\n                        else\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topBlockedDomains[i].nameIdn) + \"</td><td>\" + topBlockedDomains[i].hits.toLocaleString();\n\n                        tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDashboardTopBlockedDomainsRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"showQueryLogs('\" + topBlockedDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Show Query Logs</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"queryDnsServer('\" + topBlockedDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Query DNS Server</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-domain=\\\"\" + htmlEncode(topBlockedDomains[i].name) + \"\\\" onclick=\\\"allowDomain(this, 'btnDashboardTopBlockedDomainsRowOption'); return false;\\\">Allow Domain</a></li>\";\n                        tableHtmlRows += \"</ul></div></td></tr>\";\n                    }\n                }\n\n                $(\"#tableTopBlockedDomains\").html(tableHtmlRows);\n            }\n\n            if (!hideLoader) {\n                divDashboardLoader.hide();\n                divDashboard.show();\n            }\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divDashboardLoader,\n        dontHideAlert: hideLoader\n    });\n}\n\nfunction showTopStats(statsType, limit) {\n    var divTopStatsAlert = $(\"#divTopStatsAlert\");\n    var divTopStatsLoader = $(\"#divTopStatsLoader\");\n\n    $(\"#tableTopStatsClients\").hide();\n    $(\"#tableTopStatsDomains\").hide();\n    $(\"#tableTopStatsBlockedDomains\").hide();\n    divTopStatsLoader.show();\n\n    switch (statsType) {\n        case \"TopClients\":\n            $(\"#lblTopStatsTitle\").text(\"Top \" + limit + \" Clients\");\n            break;\n\n        case \"TopDomains\":\n            $(\"#lblTopStatsTitle\").text(\"Top \" + limit + \" Domains\");\n            break;\n\n        case \"TopBlockedDomains\":\n            $(\"#lblTopStatsTitle\").text(\"Top \" + limit + \" Blocked Domains\");\n            break;\n    }\n\n    $(\"#modalTopStats\").modal(\"show\");\n\n    var type = $(\"input[name=rdStatType]:checked\").val();\n    var custom = \"\";\n\n    if (type === \"custom\") {\n        var txtStart = $(\"#dpCustomDayWiseStart\").val();\n        if (txtStart === null || (txtStart === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please select a start date.\");\n            $(\"#dpCustomDayWiseStart\").trigger(\"focus\");\n            return;\n        }\n\n        var txtEnd = $(\"#dpCustomDayWiseEnd\").val();\n        if (txtEnd === null || (txtEnd === \"\")) {\n            showAlert(\"warning\", \"Missing!\", \"Please select an end date.\");\n            $(\"#dpCustomDayWiseEnd\").trigger(\"focus\");\n            return;\n        }\n\n        var start = moment(txtStart);\n        var end = moment(txtEnd);\n\n        if ((end.diff(start, \"days\") + 1) > 7) {\n            start = moment.utc(txtStart).toISOString();\n            end = moment.utc(txtEnd).toISOString();\n        }\n        else {\n            start = start.toISOString();\n            end = end.toISOString();\n        }\n\n        custom = \"&start=\" + encodeURIComponent(start) + \"&end=\" + encodeURIComponent(end);\n    }\n\n    var node = $(\"#optDashboardClusterNode\").val();\n\n    HTTPRequest({\n        url: \"api/dashboard/stats/getTop?token=\" + sessionData.token + \"&type=\" + type + custom + \"&statsType=\" + statsType + \"&limit=\" + limit + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            divTopStatsLoader.hide();\n\n            if (responseJSON.response.topClients != null) {\n                var tableHtmlRows;\n                var topClients = responseJSON.response.topClients;\n\n                if (topClients.length < 1) {\n                    tableHtmlRows = \"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Data</td></tr>\";\n                }\n                else {\n                    tableHtmlRows = \"\";\n\n                    for (var i = 0; i < topClients.length; i++) {\n                        tableHtmlRows += \"<tr\" + (topClients[i].rateLimited ? \" style=\\\"color: orange;\\\"\" : \"\") + \"><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topClients[i].name) + (topClients[i].rateLimited ? \" (rate limited)\" : \"\") + \"<br />\" + htmlEncode(topClients[i].domain == \"\" ? \".\" : topClients[i].domain) + \"</td><td>\" + topClients[i].hits.toLocaleString();\n                        tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDashboardTopClientsRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"showQueryLogs(null, '\" + topClients[i].name + \"', '\" + node + \"'); return false;\\\">Show Query Logs</a></li>\";\n                        tableHtmlRows += \"</ul></div></td></tr>\";\n                    }\n                }\n\n                $(\"#tbodyTopStatsClients\").html(tableHtmlRows);\n\n                if (topClients.length > 0)\n                    $(\"#tfootTopStatsClients\").html(\"Total Clients: \" + topClients.length);\n                else\n                    $(\"#tfootTopStatsClients\").html(\"\");\n\n                $(\"#tableTopStatsClients\").show();\n            }\n            else if (responseJSON.response.topDomains != null) {\n                var tableHtmlRows;\n                var topDomains = responseJSON.response.topDomains;\n\n                if (topDomains.length < 1) {\n                    tableHtmlRows = \"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Data</td></tr>\";\n                }\n                else {\n                    tableHtmlRows = \"\";\n\n                    for (var i = 0; i < topDomains.length; i++) {\n                        if (topDomains[i].nameIdn == null)\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topDomains[i].name == \"\" ? \".\" : topDomains[i].name) + \"</td><td>\" + topDomains[i].hits.toLocaleString();\n                        else\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topDomains[i].nameIdn) + \"</td><td>\" + topDomains[i].hits.toLocaleString();\n\n                        tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDashboardTopStatsDomainsRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"showQueryLogs('\" + topDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Show Query Logs</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"queryDnsServer('\" + topDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Query DNS Server</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-domain=\\\"\" + htmlEncode(topDomains[i].name) + \"\\\" onclick=\\\"blockDomain(this, 'btnDashboardTopStatsDomainsRowOption', 'divTopStatsAlert'); return false;\\\">Block Domain</a></li>\";\n                        tableHtmlRows += \"</ul></div></td></tr>\";\n                    }\n                }\n\n                $(\"#tbodyTopStatsDomains\").html(tableHtmlRows);\n\n                if (topDomains.length > 0)\n                    $(\"#tfootTopStatsDomains\").html(\"Total Domains: \" + topDomains.length);\n                else\n                    $(\"#tfootTopStatsDomains\").html(\"\");\n\n                $(\"#tableTopStatsDomains\").show();\n            }\n            else if (responseJSON.response.topBlockedDomains != null) {\n                var tableHtmlRows;\n                var topBlockedDomains = responseJSON.response.topBlockedDomains;\n\n                if (topBlockedDomains.length < 1) {\n                    tableHtmlRows = \"<tr><td colspan=\\\"3\\\" align=\\\"center\\\">No Data</td></tr>\";\n                }\n                else {\n                    tableHtmlRows = \"\";\n\n                    for (var i = 0; i < topBlockedDomains.length; i++) {\n                        if (topBlockedDomains[i].nameIdn == null)\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topBlockedDomains[i].name == \"\" ? \".\" : topBlockedDomains[i].name) + \"</td><td>\" + topBlockedDomains[i].hits.toLocaleString();\n                        else\n                            tableHtmlRows += \"<tr><td style=\\\"word-wrap: anywhere;\\\">\" + htmlEncode(topBlockedDomains[i].nameIdn) + \"</td><td>\" + topBlockedDomains[i].hits.toLocaleString();\n\n                        tableHtmlRows += \"</td><td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDashboardTopStatsBlockedDomainsRowOption\" + i + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"showQueryLogs('\" + topBlockedDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Show Query Logs</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" onclick=\\\"queryDnsServer('\" + topBlockedDomains[i].name + \"', null, '\" + node + \"'); return false;\\\">Query DNS Server</a></li>\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + i + \"\\\" data-domain=\\\"\" + htmlEncode(topBlockedDomains[i].name) + \"\\\" onclick=\\\"allowDomain(this, 'btnDashboardTopStatsBlockedDomainsRowOption', 'divTopStatsAlert'); return false;\\\">Allow Domain</a></li>\";\n                        tableHtmlRows += \"</ul></div></td></tr>\";\n                    }\n                }\n\n                $(\"#tbodyTopStatsBlockedDomains\").html(tableHtmlRows);\n\n                if (topBlockedDomains.length > 0)\n                    $(\"#tfootTopStatsBlockedDomains\").html(\"Total Domains: \" + topBlockedDomains.length);\n                else\n                    $(\"#tfootTopStatsBlockedDomains\").html(\"\");\n\n                $(\"#tableTopStatsBlockedDomains\").show();\n            }\n\n            $(\"#divTopStatsData\").animate({ scrollTop: 0 }, \"fast\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divTopStatsLoader,\n        objAlertPlaceholder: divTopStatsAlert\n    });\n}\n\nfunction resetBackupSettingsModal() {\n    $(\"#divBackupSettingsAlert\").html(\"\");\n\n    $(\"#chkBackupAuthConfig\").prop(\"checked\", true);\n    $(\"#chkBackupClusterConfig\").prop(\"checked\", true);\n    $(\"#chkBackupWebServiceConfig\").prop(\"checked\", true);\n    $(\"#chkBackupDnsConfig\").prop(\"checked\", true);\n    $(\"#chkBackupLogConfig\").prop(\"checked\", true);\n    $(\"#chkBackupZones\").prop(\"checked\", true);\n    $(\"#chkBackupAllowedZones\").prop(\"checked\", true);\n    $(\"#chkBackupBlockedZones\").prop(\"checked\", true);\n    $(\"#chkBackupBlockLists\").prop(\"checked\", true);\n    $(\"#chkBackupApps\").prop(\"checked\", true);\n    $(\"#chkBackupScopes\").prop(\"checked\", true);\n    $(\"#chkBackupStats\").prop(\"checked\", true);\n    $(\"#chkBackupLogs\").prop(\"checked\", false);\n}\n\nfunction backupSettings() {\n    var divBackupSettingsAlert = $(\"#divBackupSettingsAlert\");\n\n    var authConfig = $(\"#chkBackupAuthConfig\").prop(\"checked\");\n    var clusterConfig = $(\"#chkBackupClusterConfig\").prop(\"checked\");\n    var webServiceSettings = $(\"#chkBackupWebServiceConfig\").prop(\"checked\");\n    var dnsSettings = $(\"#chkBackupDnsConfig\").prop(\"checked\");\n    var logSettings = $(\"#chkBackupLogConfig\").prop(\"checked\");\n    var zones = $(\"#chkBackupZones\").prop(\"checked\");\n    var allowedZones = $(\"#chkBackupAllowedZones\").prop(\"checked\");\n    var blockedZones = $(\"#chkBackupBlockedZones\").prop(\"checked\");\n    var blockLists = $(\"#chkBackupBlockLists\").prop(\"checked\");\n    var apps = $(\"#chkBackupApps\").prop(\"checked\");\n    var scopes = $(\"#chkBackupScopes\").prop(\"checked\");\n    var stats = $(\"#chkBackupStats\").prop(\"checked\");\n    var logs = $(\"#chkBackupLogs\").prop(\"checked\");\n\n    if (!authConfig && !clusterConfig && !webServiceSettings && !dnsSettings && !logSettings && !zones && !allowedZones && !blockedZones && !blockLists && !apps && !scopes && !stats && !logs) {\n        showAlert(\"warning\", \"Missing!\", \"Please select at least one item to backup.\", divBackupSettingsAlert);\n        return;\n    }\n\n    var node = $(\"#optSettingsClusterNode\").val();\n\n    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\");\n\n    $(\"#modalBackupSettings\").modal(\"hide\");\n    showAlert(\"success\", \"Backed Up!\", \"Settings were backed up successfully.\");\n}\n\nfunction resetRestoreSettingsModal() {\n    $(\"#divRestoreSettingsAlert\").html(\"\");\n\n    $(\"#fileBackupZip\").val(\"\");\n\n    $(\"#chkRestoreAuthConfig\").prop(\"checked\", true);\n    $(\"#chkRestoreClusterConfig\").prop(\"checked\", true);\n    $(\"#chkRestoreWebServiceConfig\").prop(\"checked\", true);\n    $(\"#chkRestoreDnsConfig\").prop(\"checked\", true);\n    $(\"#chkRestoreLogConfig\").prop(\"checked\", true);\n    $(\"#chkRestoreZones\").prop(\"checked\", true);\n    $(\"#chkRestoreAllowedZones\").prop(\"checked\", true);\n    $(\"#chkRestoreBlockedZones\").prop(\"checked\", true);\n    $(\"#chkRestoreBlockLists\").prop(\"checked\", true);\n    $(\"#chkRestoreApps\").prop(\"checked\", true);\n    $(\"#chkRestoreScopes\").prop(\"checked\", true);\n    $(\"#chkRestoreStats\").prop(\"checked\", true);\n    $(\"#chkRestoreLogs\").prop(\"checked\", false);\n    $(\"#chkDeleteExistingFiles\").prop(\"checked\", true);\n}\n\nfunction restoreSettings() {\n    var divRestoreSettingsAlert = $(\"#divRestoreSettingsAlert\");\n\n    var fileBackupZip = $(\"#fileBackupZip\");\n\n    if (fileBackupZip[0].files.length === 0) {\n        showAlert(\"warning\", \"Missing!\", \"Please select a backup zip file to restore.\", divRestoreSettingsAlert);\n        fileBackupZip.trigger(\"focus\");\n        return;\n    }\n\n    var authConfig = $(\"#chkRestoreAuthConfig\").prop(\"checked\");\n    var clusterConfig = $(\"#chkRestoreClusterConfig\").prop(\"checked\");\n    var webServiceSettings = $(\"#chkRestoreWebServiceConfig\").prop(\"checked\");\n    var dnsSettings = $(\"#chkRestoreDnsConfig\").prop(\"checked\");\n    var logSettings = $(\"#chkRestoreLogConfig\").prop(\"checked\");\n    var zones = $(\"#chkRestoreZones\").prop(\"checked\");\n    var allowedZones = $(\"#chkRestoreAllowedZones\").prop(\"checked\");\n    var blockedZones = $(\"#chkRestoreBlockedZones\").prop(\"checked\");\n    var blockLists = $(\"#chkRestoreBlockLists\").prop(\"checked\");\n    var apps = $(\"#chkRestoreApps\").prop(\"checked\");\n    var scopes = $(\"#chkRestoreScopes\").prop(\"checked\");\n    var stats = $(\"#chkRestoreStats\").prop(\"checked\");\n    var logs = $(\"#chkRestoreLogs\").prop(\"checked\");\n\n    var deleteExistingFiles = $(\"#chkDeleteExistingFiles\").prop(\"checked\");\n\n    if (!authConfig && !clusterConfig && !webServiceSettings && !dnsSettings && !logSettings && !zones && !allowedZones && !blockedZones && !blockLists && !apps && !scopes && !stats && !logs) {\n        showAlert(\"warning\", \"Missing!\", \"Please select at least one item to restore.\", divRestoreSettingsAlert);\n        return;\n    }\n\n    var formData = new FormData();\n    formData.append(\"fileBackupZip\", $(\"#fileBackupZip\")[0].files[0]);\n\n    var node = $(\"#optSettingsClusterNode\").val();\n\n    var btn = $(\"#btnRestoreSettings\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        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),\n        method: \"POST\",\n        data: formData,\n        contentType: false,\n        processData: false,\n        success: function (responseJSON) {\n            if ((node == \"\") || (node == sessionData.info.dnsServerDomain))\n                updateDnsSettingsDataAndGui(responseJSON);\n\n            loadDnsSettings(responseJSON);\n\n            $(\"#modalRestoreSettings\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Restored!\", \"Settings were restored successfully.\");\n\n            if (sessionData.info.dnsServerDomain == responseJSON.server)\n                checkForWebConsoleRedirection(responseJSON);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divRestoreSettingsAlert\n    });\n}\n\nfunction applyTheme() {\n    const currentTheme = localStorage.getItem(\"theme\");\n\n    if (currentTheme === \"dark\")\n        document.body.classList.add(\"dark-mode\");\n    else\n        document.body.classList.remove(\"dark-mode\");\n}\n\nfunction toggleTheme() {\n    document.body.classList.toggle(\"dark-mode\");\n\n    let theme = \"light\";\n\n    if (document.body.classList.contains(\"dark-mode\"))\n        theme = \"dark\";\n\n    localStorage.setItem(\"theme\", theme);\n\n    if (window.chartDashboardMain) {\n        window.chartDashboardMain.update();\n        window.chartDashboardPie.update();\n        window.chartDashboardPie2.update();\n        window.chartDashboardPie3.update();\n    }\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/other-zones.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nfunction flushDnsCache(objBtn, node) {\n    if (!confirm(\"Are you sure to flush the DNS Server cache?\"))\n        return;\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/cache/flush?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#lstCachedZones\").html(\"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshCachedZonesList(); return false;\\\"><b>[refresh]</b></a></div>\");\n            $(\"#txtCachedZoneViewerTitle\").text(\"<ROOT>\");\n            $(\"#btnDeleteCachedZone\").hide();\n            $(\"#preCachedZoneViewerBody\").hide();\n\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Flushed!\", \"DNS Server cache was flushed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteCachedZone() {\n    var domain = $(\"#txtCachedZoneViewerTitle\").text();\n\n    if (!confirm(\"Are you sure you want to delete the cached zone '\" + domain + \"' and all its records?\"))\n        return;\n\n    var node = $(\"#optCachedZonesClusterNode\").val();\n\n    var btn = $(\"#btnDeleteCachedZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/cache/delete?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshCachedZonesList(getParentDomain(domain), \"up\");\n\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Deleted!\", \"Cached zone '\" + domain + \"' was deleted successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction getParentDomain(domain) {\n\n    if ((domain != null) && (domain != \"\")) {\n        var parentDomain;\n        var i = domain.indexOf(\".\");\n\n        if (i == -1)\n            parentDomain = \"\";\n        else\n            parentDomain = domain.substr(i + 1);\n\n        return parentDomain;\n    }\n\n    return null;\n}\n\nfunction refreshCachedZonesList(domain, direction) {\n    if (domain == null) {\n        domain = $(\"#txtCachedZoneViewerTitle\").text();\n\n        if ((domain == null) || (domain == \"<ROOT>\"))\n            domain = \"\";\n    }\n\n    domain.toLowerCase();\n\n    var node = $(\"#optCachedZonesClusterNode\").val();\n\n    var lstCachedZones = $(\"#lstCachedZones\");\n    var divCachedZoneViewer = $(\"#divCachedZoneViewer\");\n    var preCachedZoneViewerBody = $(\"#preCachedZoneViewerBody\");\n\n    divCachedZoneViewer.hide();\n    preCachedZoneViewerBody.hide();\n\n    HTTPRequest({\n        url: \"api/cache/list?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain) + ((direction == null) ? \"\" : \"&direction=\" + direction) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var newDomain = responseJSON.response.domain;\n            var zones = responseJSON.response.zones;\n\n            var list = \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshCachedZonesList('\" + newDomain + \"'); return false;\\\"><b>[refresh]</b></a></div>\";\n\n            var parentDomain = getParentDomain(newDomain);\n\n            if (parentDomain != null)\n                list += \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshCachedZonesList('\" + parentDomain + \"', 'up'); return false;\\\"><b>[up]</b></a></div>\";\n\n            for (var i = 0; i < zones.length; i++) {\n                var zoneName = htmlEncode(zones[i]);\n\n                list += \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshCachedZonesList('\" + zoneName + \"'); return false;\\\">\" + zoneName + \"</a></div>\";\n            }\n\n            lstCachedZones.html(list);\n\n            if (newDomain == \"\") {\n                $(\"#txtCachedZoneViewerTitle\").text(\"<ROOT>\");\n                $(\"#btnDeleteCachedZone\").hide();\n            }\n            else {\n                if (responseJSON.response.domainIdn == null)\n                    $(\"#txtCachedZoneViewerTitle\").text(newDomain);\n                else\n                    $(\"#txtCachedZoneViewerTitle\").text(responseJSON.response.domainIdn);\n\n                $(\"#btnDeleteCachedZone\").show();\n            }\n\n            if (responseJSON.response.records.length > 0) {\n                preCachedZoneViewerBody.text(JSON.stringify(responseJSON.response.records, null, 2));\n                preCachedZoneViewerBody.show();\n            }\n\n            divCachedZoneViewer.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        error: function () {\n            lstCachedZones.html(\"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshCachedZonesList('\" + domain + \"'); return false;\\\"><b>[refresh]</b></a></div>\");\n\n            divCachedZoneViewer.show();\n        },\n        objLoaderPlaceholder: lstCachedZones\n    });\n}\n\nfunction allowZone() {\n    var domain = $(\"#txtAllowZone\").val();\n\n    if ((domain === null) || (domain === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to allow.\");\n        $(\"#txtAllowZone\").trigger(\"focus\");\n        return;\n    }\n\n    var btn = $(\"#btnAllowZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/allowed/add?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n        success: function (responseJSON) {\n            refreshAllowedZonesList(domain, null, true);\n\n            $(\"#txtAllowZone\").val(\"\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Allowed!\", \"Domain '\" + domain + \"' was added to Allowed Zone successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteAllowedZone() {\n    var domain = $(\"#txtAllowedZoneViewerTitle\").text();\n\n    if (!confirm(\"Are you sure you want to delete the allowed zone '\" + domain + \"'?\"))\n        return;\n\n    var btn = $(\"#btnDeleteAllowedZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/allowed/delete?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n        success: function (responseJSON) {\n            refreshAllowedZonesList(getParentDomain(domain), \"up\", true);\n\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Deleted!\", \"Domain '\" + domain + \"' was deleted from Allowed Zone successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction flushAllowedZone() {\n    if (!confirm(\"Are you sure you want to flush the entire Allowed zone?\"))\n        return;\n\n    var btn = $(\"#btnFlushAllowedZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/allowed/flush?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            $(\"#lstAllowedZones\").html(\"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshAllowedZonesList(); return false;\\\"><b>[refresh]</b></a></div>\");\n            $(\"#txtAllowedZoneViewerTitle\").text(\"<ROOT>\");\n            $(\"#btnDeleteAllowedZone\").hide();\n            $(\"#preAllowedZoneViewerBody\").hide();\n\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Flushed!\", \"Allowed zone was flushed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction refreshAllowedZonesList(domain, direction, fromPrimary) {\n    if (domain == null) {\n        domain = $(\"#txtAllowedZoneViewerTitle\").text();\n\n        if ((domain == null) || (domain == \"<ROOT>\"))\n            domain = \"\";\n    }\n\n    domain.toLowerCase();\n\n    var node = fromPrimary ? getPrimaryClusterNodeName() : \"\";\n\n    var lstAllowedZones = $(\"#lstAllowedZones\");\n    var divAllowedZoneViewer = $(\"#divAllowedZoneViewer\");\n    var preAllowedZoneViewerBody = $(\"#preAllowedZoneViewerBody\");\n\n    divAllowedZoneViewer.hide();\n    preAllowedZoneViewerBody.hide();\n\n    HTTPRequest({\n        url: \"api/allowed/list?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain) + ((direction == null) ? \"\" : \"&direction=\" + direction) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var newDomain = responseJSON.response.domain;\n            var zones = responseJSON.response.zones;\n\n            var list = \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshAllowedZonesList('\" + newDomain + \"'); return false;\\\"><b>[refresh]</b></a></div>\";\n\n            var parentDomain = getParentDomain(newDomain);\n\n            if (parentDomain != null)\n                list += \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshAllowedZonesList('\" + parentDomain + \"', 'up'); return false;\\\"><b>[up]</b></a></div>\";\n\n            for (var i = 0; i < zones.length; i++) {\n                var zoneName = htmlEncode(zones[i]);\n\n                list += \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshAllowedZonesList('\" + zoneName + \"'); return false;\\\">\" + zoneName + \"</a></div>\";\n            }\n\n            lstAllowedZones.html(list);\n\n            if (newDomain == \"\") {\n                $(\"#txtAllowedZoneViewerTitle\").text(\"<ROOT>\");\n            }\n            else {\n                if (responseJSON.response.domainIdn == null)\n                    $(\"#txtAllowedZoneViewerTitle\").text(newDomain);\n                else\n                    $(\"#txtAllowedZoneViewerTitle\").text(responseJSON.response.domainIdn);\n            }\n\n            if (responseJSON.response.records.length > 0) {\n                preAllowedZoneViewerBody.text(JSON.stringify(responseJSON.response.records, null, 2));\n                preAllowedZoneViewerBody.show();\n\n                $(\"#btnDeleteAllowedZone\").show();\n            }\n            else {\n                $(\"#btnDeleteAllowedZone\").hide();\n            }\n\n            divAllowedZoneViewer.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        error: function () {\n            lstAllowedZones.html(\"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshAllowedZonesList('\" + domain + \"'); return false;\\\"><b>[refresh]</b></a></div>\");\n\n            divAllowedZoneViewer.show();\n        },\n        objLoaderPlaceholder: lstAllowedZones\n    });\n}\n\nfunction blockZone() {\n    var domain = $(\"#txtBlockZone\").val();\n\n    if ((domain === null) || (domain === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to block.\");\n        $(\"#txtBlockZone\").trigger(\"focus\");\n        return;\n    }\n\n    var btn = $(\"#btnBlockZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/blocked/add?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n        success: function (responseJSON) {\n            refreshBlockedZonesList(domain, null, true);\n\n            $(\"#txtBlockZone\").val(\"\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Blocked!\", \"Domain '\" + domain + \"' was added to Blocked Zone successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteBlockedZone() {\n    var domain = $(\"#txtBlockedZoneViewerTitle\").text();\n\n    if (!confirm(\"Are you sure you want to delete the blocked zone '\" + domain + \"'?\"))\n        return;\n\n    var btn = $(\"#btnDeleteBlockedZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/blocked/delete?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n        success: function (responseJSON) {\n            refreshBlockedZonesList(getParentDomain(domain), \"up\", true);\n\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Deleted!\", \"Blocked zone '\" + domain + \"' was deleted successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction flushBlockedZone() {\n    if (!confirm(\"Are you sure you want to flush the entire Blocked zone?\"))\n        return;\n\n    var btn = $(\"#btnFlushBlockedZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/blocked/flush?token=\" + sessionData.token,\n        success: function (responseJSON) {\n            $(\"#lstBlockedZones\").html(\"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshBlockedZonesList(); return false;\\\"><b>[refresh]</b></a></div>\");\n            $(\"#txtBlockedZoneViewerTitle\").text(\"<ROOT>\");\n            $(\"#btnDeleteBlockedZone\").hide();\n            $(\"#preBlockedZoneViewerBody\").hide();\n\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Flushed!\", \"Blocked zone was flushed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction refreshBlockedZonesList(domain, direction, fromPrimary) {\n    if (domain == null) {\n        domain = $(\"#txtBlockedZoneViewerTitle\").text();\n\n        if ((domain == null) || (domain == \"<ROOT>\"))\n            domain = \"\";\n    }\n\n    domain.toLowerCase();\n\n    var node = fromPrimary ? getPrimaryClusterNodeName() : \"\";\n\n    var lstBlockedZones = $(\"#lstBlockedZones\");\n    var divBlockedZoneViewer = $(\"#divBlockedZoneViewer\");\n    var preBlockedZoneViewerBody = $(\"#preBlockedZoneViewerBody\");\n\n    divBlockedZoneViewer.hide();\n    preBlockedZoneViewerBody.hide();\n\n    HTTPRequest({\n        url: \"api/blocked/list?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain) + ((direction == null) ? \"\" : \"&direction=\" + direction) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var newDomain = responseJSON.response.domain;\n            var zones = responseJSON.response.zones;\n\n            var list = \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshBlockedZonesList('\" + newDomain + \"'); return false;\\\"><b>[refresh]</b></a></div>\";\n\n            var parentDomain = getParentDomain(newDomain);\n\n            if (parentDomain != null)\n                list += \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshBlockedZonesList('\" + parentDomain + \"', 'up'); return false;\\\"><b>[up]</b></a></div>\";\n\n            for (var i = 0; i < zones.length; i++) {\n                var zoneName = htmlEncode(zones[i]);\n\n                list += \"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshBlockedZonesList('\" + zoneName + \"'); return false;\\\">\" + zoneName + \"</a></div>\";\n            }\n\n            lstBlockedZones.html(list);\n\n            if (newDomain == \"\") {\n                $(\"#txtBlockedZoneViewerTitle\").text(\"<ROOT>\");\n            }\n            else {\n                if (responseJSON.response.domainIdn == null)\n                    $(\"#txtBlockedZoneViewerTitle\").text(newDomain);\n                else\n                    $(\"#txtBlockedZoneViewerTitle\").text(responseJSON.response.domainIdn);\n            }\n\n            if (responseJSON.response.records.length > 0) {\n                preBlockedZoneViewerBody.text(JSON.stringify(responseJSON.response.records, null, 2));\n                preBlockedZoneViewerBody.show();\n\n                $(\"#btnDeleteBlockedZone\").show();\n            }\n            else {\n                $(\"#btnDeleteBlockedZone\").hide();\n            }\n\n            divBlockedZoneViewer.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        error: function () {\n            lstBlockedZones.html(\"<div class=\\\"zone\\\"><a href=\\\"#\\\" onclick=\\\"refreshBlockedZonesList('\" + domain + \"'); return false;\\\"><b>[refresh]</b></a></div>\");\n\n            divBlockedZoneViewer.show();\n        },\n        objLoaderPlaceholder: lstBlockedZones\n    });\n}\n\nfunction resetImportAllowedZonesModal() {\n    $(\"#divImportAllowedZonesAlert\").html(\"\");\n    $(\"#txtImportAllowedZones\").val(\"\");\n\n    setTimeout(function () {\n        $(\"#txtImportAllowedZones\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction importAllowedZones() {\n    var divImportAllowedZonesAlert = $(\"#divImportAllowedZonesAlert\");\n    var allowedZones = cleanTextList($(\"#txtImportAllowedZones\").val());\n\n    if ((allowedZones.length === 0) || (allowedZones === \",\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter allowed zones to import.\", divImportAllowedZonesAlert);\n        $(\"#txtImportAllowedZones\").trigger(\"focus\");\n        return;\n    }\n\n    var btn = $(\"#btnImportAllowedZones\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/allowed/import?token=\" + sessionData.token,\n        method: \"POST\",\n        data: \"allowedZones=\" + encodeURIComponent(allowedZones),\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalImportAllowedZones\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Imported!\", \"Domain names were imported into allowed zone successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divImportAllowedZonesAlert\n    });\n}\n\nfunction exportAllowedZones() {\n    window.open(\"api/allowed/export?token=\" + sessionData.token, \"_blank\");\n\n    showAlert(\"success\", \"Exported!\", \"Allowed zones were exported successfully.\");\n}\n\nfunction resetImportBlockedZonesModal() {\n    $(\"#divImportBlockedZonesAlert\").html(\"\");\n    $(\"#txtImportBlockedZones\").val(\"\");\n\n    setTimeout(function () {\n        $(\"#txtImportBlockedZones\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction importBlockedZones() {\n    var divImportBlockedZonesAlert = $(\"#divImportBlockedZonesAlert\");\n    var blockedZones = cleanTextList($(\"#txtImportBlockedZones\").val());\n\n    if ((blockedZones.length === 0) || (blockedZones === \",\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter blocked zones to import.\", divImportBlockedZonesAlert);\n        $(\"#txtImportBlockedZones\").trigger(\"focus\");\n        return;\n    }\n\n    var btn = $(\"#btnImportBlockedZones\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/blocked/import?token=\" + sessionData.token,\n        method: \"POST\",\n        data: \"blockedZones=\" + encodeURIComponent(blockedZones),\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalImportBlockedZones\").modal(\"hide\");\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Imported!\", \"Domain names were imported into blocked zone successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divImportBlockedZonesAlert\n    });\n}\n\nfunction exportBlockedZones() {\n    window.open(\"api/blocked/export?token=\" + sessionData.token, \"_blank\");\n\n    showAlert(\"success\", \"Exported!\", \"Blocked zones were exported successfully.\");\n}\n\nfunction allowDomain(objMenuItem, btnName, alertPlaceholderName) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var domain = mnuItem.attr(\"data-domain\");\n\n    var btn = $(\"#\" + btnName + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    var alertPlaceholder;\n    if (alertPlaceholderName != null)\n        alertPlaceholder = $(\"#\" + alertPlaceholderName);\n\n    HTTPRequest({\n        url: \"api/blocked/delete?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n        success: function (responseJSON) {\n            HTTPRequest({\n                url: \"api/allowed/add?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n                success: function (responseJSON) {\n                    btn.prop(\"disabled\", false);\n                    btn.html(originalBtnHtml);\n\n                    showAlert(\"success\", \"Allowed!\", \"Domain '\" + domain + \"' was added to Allowed Zone successfully.\", alertPlaceholder);\n                },\n                error: function () {\n                    btn.prop(\"disabled\", false);\n                    btn.html(originalBtnHtml);\n                },\n                invalidToken: function () {\n                    showPageLogin();\n                },\n                objAlertPlaceholder: alertPlaceholder\n            });\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objAlertPlaceholder: alertPlaceholder\n    });\n}\n\nfunction blockDomain(objMenuItem, btnName, alertPlaceholderName) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var domain = mnuItem.attr(\"data-domain\");\n\n    var btn = $(\"#\" + btnName + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    var alertPlaceholder;\n    if (alertPlaceholderName != null)\n        alertPlaceholder = $(\"#\" + alertPlaceholderName);\n\n    HTTPRequest({\n        url: \"api/allowed/delete?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n        success: function (responseJSON) {\n            HTTPRequest({\n                url: \"api/blocked/add?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(domain),\n                success: function (responseJSON) {\n                    btn.prop(\"disabled\", false);\n                    btn.html(originalBtnHtml);\n\n                    showAlert(\"success\", \"Blocked!\", \"Domain '\" + domain + \"' was added to Blocked Zone successfully.\", alertPlaceholder);\n                },\n                error: function () {\n                    btn.prop(\"disabled\", false);\n                    btn.html(originalBtnHtml);\n                },\n                invalidToken: function () {\n                    showPageLogin();\n                },\n                objAlertPlaceholder: alertPlaceholder\n            });\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objAlertPlaceholder: alertPlaceholder\n    });\n}\n"
  },
  {
    "path": "DnsServerCore/www/js/zone.js",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nvar zoneOptionsAvailableTsigKeyNames;\nvar editZoneInfo;\nvar editZoneRecords;\nvar editZoneFilteredRecords;\n\n$(function () {\n    $(\"input[type=radio][name=rdAddZoneType]\").on(\"change\", function () {\n        $(\"#txtAddZone\").prop(\"disabled\", false);\n        $(\"#divAddZoneCatalogZone\").hide();\n        $(\"#divAddZoneInitializeForwarder\").hide();\n        $(\"#divAddZoneImportZoneFile\").hide();\n        $(\"#divAddZoneUseSoaSerialDateScheme\").hide();\n        $(\"#divAddZonePrimaryNameServerAddresses\").hide();\n        $(\"#lblAddZonePrimaryNameServerAddresses\").text(\"Primary Name Server Addresses (Optional)\");\n        $(\"#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.\");\n        $(\"#divAddZoneZoneTransferProtocol\").hide();\n        $(\"#divAddZoneTsigKeyName\").hide();\n        $(\"#divAddZoneValidateZone\").hide();\n        $(\"#divAddZoneForwarderProtocol\").hide();\n        $(\"#divAddZoneForwarder\").hide();\n        $(\"#divAddZoneForwarderDnssecValidation\").hide();\n        $(\"#divAddZoneForwarderProxy\").hide();\n\n        var zoneType = $('input[name=rdAddZoneType]:checked').val();\n        switch (zoneType) {\n            case \"Primary\":\n                if ($(\"#optAddZoneCatalogZoneName\").attr(\"hasItems\") == \"true\")\n                    $(\"#divAddZoneCatalogZone\").show();\n\n                $(\"#divAddZoneImportZoneFile\").show();\n                $(\"#divAddZoneUseSoaSerialDateScheme\").show();\n                break;\n\n            case \"Secondary\":\n                if ($(\"#optAddZoneCatalogZoneName\").attr(\"hasItems\") == \"true\")\n                    $(\"#divAddZoneCatalogZone\").show();\n\n                $(\"#divAddZonePrimaryNameServerAddresses\").show();\n                $(\"#divAddZoneZoneTransferProtocol\").show();\n                $(\"#divAddZoneTsigKeyName\").show();\n                $(\"#divAddZoneValidateZone\").show();\n\n                loadTsigKeyNames($(\"#optAddZoneTsigKeyName\"), null, $(\"#divAddZoneAlert\"));\n                break;\n\n            case \"Stub\":\n                if ($(\"#optAddZoneCatalogZoneName\").attr(\"hasItems\") == \"true\")\n                    $(\"#divAddZoneCatalogZone\").show();\n\n                $(\"#divAddZonePrimaryNameServerAddresses\").show();\n                break;\n\n            case \"Forwarder\":\n                if ($(\"#optAddZoneCatalogZoneName\").attr(\"hasItems\") == \"true\")\n                    $(\"#divAddZoneCatalogZone\").show();\n\n                $(\"#divAddZoneInitializeForwarder\").show();\n\n                var initializeForwarder = $(\"#chkAddZoneInitializeForwarder\").prop(\"checked\");\n\n                if (initializeForwarder) {\n                    $(\"#divAddZoneImportZoneFile\").hide();\n\n                    $(\"#divAddZoneForwarderProtocol\").show();\n                    $(\"#divAddZoneForwarder\").show();\n                    $(\"#divAddZoneForwarderDnssecValidation\").show();\n                    $(\"#divAddZoneForwarderProxy\").show();\n                } else {\n                    $(\"#divAddZoneImportZoneFile\").show();\n\n                    $(\"#divAddZoneForwarderProtocol\").hide();\n                    $(\"#divAddZoneForwarder\").hide();\n                    $(\"#divAddZoneForwarderDnssecValidation\").hide();\n                    $(\"#divAddZoneForwarderProxy\").hide();\n                }\n\n                break;\n\n            case \"SecondaryForwarder\":\n            case \"SecondaryCatalog\":\n                $(\"#lblAddZonePrimaryNameServerAddresses\").text(\"Primary Name Server Addresses\");\n                $(\"#divAddZonePrimaryNameServerAddressesInfo\").text(\"Enter the primary name server addresses to sync the zone from.\");\n                $(\"#divAddZonePrimaryNameServerAddresses\").show();\n                $(\"#divAddZoneZoneTransferProtocol\").show();\n                $(\"#divAddZoneTsigKeyName\").show();\n\n                loadTsigKeyNames($(\"#optAddZoneTsigKeyName\"), null, $(\"#divAddZoneAlert\"));\n                break;\n\n            case \"SecondaryRoot\":\n                if ($(\"#optAddZoneCatalogZoneName\").attr(\"hasItems\") == \"true\")\n                    $(\"#divAddZoneCatalogZone\").show();\n\n                $(\"#txtAddZone\").prop(\"disabled\", true);\n                $(\"#txtAddZone\").val(\".\");\n                break;\n        }\n    });\n\n    $(\"#chkAddZoneInitializeForwarder\").on(\"click\", function () {\n        var initializeForwarder = $(\"#chkAddZoneInitializeForwarder\").prop(\"checked\");\n\n        if (initializeForwarder) {\n            $(\"#divAddZoneImportZoneFile\").hide();\n\n            $(\"#divAddZoneForwarderProtocol\").show();\n            $(\"#divAddZoneForwarder\").show();\n            $(\"#divAddZoneForwarderDnssecValidation\").show();\n            $(\"#divAddZoneForwarderProxy\").show();\n        } else {\n            $(\"#divAddZoneImportZoneFile\").show();\n\n            $(\"#divAddZoneForwarderProtocol\").hide();\n            $(\"#divAddZoneForwarder\").hide();\n            $(\"#divAddZoneForwarderDnssecValidation\").hide();\n            $(\"#divAddZoneForwarderProxy\").hide();\n        }\n    });\n\n    $(\"input[type=radio][name=rdAddZoneForwarderProtocol]\").on(\"change\", function () {\n        var protocol = $('input[name=rdAddZoneForwarderProtocol]:checked').val();\n        switch (protocol) {\n            case \"Udp\":\n            case \"Tcp\":\n                $(\"#txtAddZoneForwarder\").attr(\"placeholder\", \"8.8.8.8 or [2620:fe::10]\")\n                break;\n\n            case \"Tls\":\n            case \"Quic\":\n                $(\"#txtAddZoneForwarder\").attr(\"placeholder\", \"dns.quad9.net (9.9.9.9:853)\")\n                break;\n\n            case \"Https\":\n                $(\"#txtAddZoneForwarder\").attr(\"placeholder\", \"https://cloudflare-dns.com/dns-query (1.1.1.1)\")\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdAddZoneForwarderProxyType]\").on(\"change\", function () {\n        var proxyType = $('input[name=rdAddZoneForwarderProxyType]:checked').val();\n        var disabled = (proxyType === \"NoProxy\") || (proxyType === \"DefaultProxy\");\n\n        $(\"#txtAddZoneForwarderProxyAddress\").prop(\"disabled\", disabled);\n        $(\"#txtAddZoneForwarderProxyPort\").prop(\"disabled\", disabled);\n        $(\"#txtAddZoneForwarderProxyUsername\").prop(\"disabled\", disabled);\n        $(\"#txtAddZoneForwarderProxyPassword\").prop(\"disabled\", disabled);\n    });\n\n    $(\"#txtEditZoneFilterName\").on(\"input\", function () {\n        editZoneFilteredRecords = null; //to evaluate filters again\n    });\n\n    $(\"#txtEditZoneFilterType\").on(\"input\", function () {\n        editZoneFilteredRecords = null; //to evaluate filters again\n    });\n\n    $(\"input[type=radio][name=rdImportZoneType]\").on(\"change\", function () {\n        var rdImportZoneType = $(\"input[name=rdImportZoneType]:checked\").val();\n        switch (rdImportZoneType) {\n            case \"File\":\n                $(\"#divImportZoneFile\").show();\n                $(\"#divImportZoneTextEditor\").hide();\n                break;\n\n            case \"Text\":\n                $(\"#divImportZoneFile\").hide();\n                $(\"#divImportZoneTextEditor\").show();\n                break;\n        }\n    });\n\n    $(\"#optZoneOptionsCatalogZoneName\").on(\"change\", function () {\n        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\", false);\n        $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"checked\", false);\n        $(\"#chkZoneOptionsCatalogOverrideNotify\").prop(\"checked\", false);\n\n        var catalog = $(\"#optZoneOptionsCatalogZoneName\").val();\n        if (catalog === \"\") {\n            $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", true);\n            $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"disabled\", true);\n            $(\"#chkZoneOptionsCatalogOverrideNotify\").prop(\"disabled\", true);\n\n            switch ($(\"#lblZoneOptionsZoneName\").attr(\"data-zone-type\")) {\n                case \"Primary\":\n                case \"Forwarder\":\n                    $(\"#tabListZoneOptionsQueryAccess\").show();\n                    $(\"#tabListZoneOptionsZoneTranfer\").show();\n                    $(\"#tabListZoneOptionsNotify\").show();\n                    break;\n\n                case \"Secondary\":\n                    $(\"#tabListZoneOptionsQueryAccess\").show();\n                    $(\"#tabListZoneOptionsZoneTranfer\").show();\n                    $(\"#tabListZoneOptionsNotify\").show();\n                    break;\n\n                case \"Stub\":\n                    $(\"#tabListZoneOptionsQueryAccess\").show();\n                    break;\n            }\n        }\n        else {\n            switch ($(\"#lblZoneOptionsZoneName\").attr(\"data-zone-type\")) {\n                case \"Primary\":\n                case \"Forwarder\":\n                    $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", false);\n                    $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"disabled\", false);\n                    $(\"#chkZoneOptionsCatalogOverrideNotify\").prop(\"disabled\", false);\n\n                    $(\"#tabListZoneOptionsQueryAccess\").hide();\n                    $(\"#tabListZoneOptionsZoneTranfer\").hide();\n                    $(\"#tabListZoneOptionsNotify\").hide();\n                    break;\n\n                case \"Secondary\":\n                    $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", false);\n                    $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"disabled\", false);\n\n                    $(\"#tabListZoneOptionsQueryAccess\").hide();\n                    $(\"#tabListZoneOptionsZoneTranfer\").hide();\n                    break;\n\n                case \"Stub\":\n                    $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", false);\n\n                    $(\"#tabListZoneOptionsQueryAccess\").hide();\n                    $(\"#tabListZoneOptionsZoneTranfer\").hide();\n                    $(\"#tabListZoneOptionsNotify\").hide();\n                    break;\n            }\n        }\n    });\n\n    $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").on(\"click\", function () {\n        var checked = $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\");\n\n        if (checked)\n            $(\"#tabListZoneOptionsQueryAccess\").show();\n        else\n            $(\"#tabListZoneOptionsQueryAccess\").hide();\n    });\n\n    $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").on(\"click\", function () {\n        var checked = $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"checked\");\n\n        if (checked)\n            $(\"#tabListZoneOptionsZoneTranfer\").show();\n        else\n            $(\"#tabListZoneOptionsZoneTranfer\").hide();\n    });\n\n    $(\"#chkZoneOptionsCatalogOverrideNotify\").on(\"click\", function () {\n        var checked = $(\"#chkZoneOptionsCatalogOverrideNotify\").prop(\"checked\");\n\n        if (checked)\n            $(\"#tabListZoneOptionsNotify\").show();\n        else\n            $(\"#tabListZoneOptionsNotify\").hide();\n    });\n\n    $(\"input[type=radio][name=rdQueryAccess]\").on(\"change\", function () {\n        var queryAccess = $(\"input[name=rdQueryAccess]:checked\").val();\n        switch (queryAccess) {\n            case \"UseSpecifiedNetworkACL\":\n            case \"AllowZoneNameServersAndUseSpecifiedNetworkACL\":\n                $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", false);\n                break;\n\n            default:\n                $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", true);\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdZoneTransfer]\").on(\"change\", function () {\n        var zoneTransfer = $('input[name=rdZoneTransfer]:checked').val();\n        switch (zoneTransfer) {\n            case \"UseSpecifiedNetworkACL\":\n            case \"AllowZoneNameServersAndUseSpecifiedNetworkACL\":\n                $(\"#txtZoneTransferNetworkACL\").prop(\"disabled\", false);\n                break;\n\n            default:\n                $(\"#txtZoneTransferNetworkACL\").prop(\"disabled\", true);\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdZoneNotify]\").on(\"change\", function () {\n        var zoneNotify = $('input[name=rdZoneNotify]:checked').val();\n        switch (zoneNotify) {\n            case \"SpecifiedNameServers\":\n            case \"BothZoneAndSpecifiedNameServers\":\n                $(\"#txtZoneNotifyNameServers\").prop(\"disabled\", false);\n                $(\"#txtZoneNotifySecondaryCatalogNameServers\").prop(\"disabled\", true);\n                break;\n\n            case \"SeparateNameServersForCatalogAndMemberZones\":\n                $(\"#txtZoneNotifyNameServers\").prop(\"disabled\", false);\n                $(\"#txtZoneNotifySecondaryCatalogNameServers\").prop(\"disabled\", false);\n                break;\n\n            default:\n                $(\"#txtZoneNotifyNameServers\").prop(\"disabled\", true);\n                $(\"#txtZoneNotifySecondaryCatalogNameServers\").prop(\"disabled\", true);\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdDynamicUpdate]\").on(\"change\", function () {\n        var dynamicUpdate = $('input[name=rdDynamicUpdate]:checked').val();\n        switch (dynamicUpdate) {\n            case \"UseSpecifiedNetworkACL\":\n            case \"AllowZoneNameServersAndUseSpecifiedNetworkACL\":\n                $(\"#txtDynamicUpdateNetworkACL\").prop(\"disabled\", false);\n                break;\n\n            default:\n                $(\"#txtDynamicUpdateNetworkACL\").prop(\"disabled\", true);\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdDnssecSignZoneAlgorithm]\").on(\"change\", function () {\n        var algorithm = $(\"input[name=rdDnssecSignZoneAlgorithm]:checked\").val();\n        switch (algorithm) {\n            case \"RSA\":\n                $(\"#divDnssecSignZoneRsaParameters\").show();\n                $(\"#divDnssecSignZoneEcdsaParameters\").hide();\n                $(\"#divDnssecSignZoneEddsaParameters\").hide();\n\n                if ($(\"input[name=rdDnssecSignZoneKskGeneration]:checked\").val() === \"Automatic\")\n                    $(\"#divDnssecSignZoneRsaKskKeySize\").show();\n                else\n                    $(\"#divDnssecSignZoneRsaKskKeySize\").hide();\n\n                if ($(\"input[name=rdDnssecSignZoneZskGeneration]:checked\").val() === \"Automatic\")\n                    $(\"#divDnssecSignZoneRsaZskKeySize\").show();\n                else\n                    $(\"#divDnssecSignZoneRsaZskKeySize\").hide();\n\n                break;\n\n            case \"ECDSA\":\n                $(\"#divDnssecSignZoneRsaParameters\").hide();\n                $(\"#divDnssecSignZoneEcdsaParameters\").show();\n                $(\"#divDnssecSignZoneEddsaParameters\").hide();\n\n                $(\"#divDnssecSignZoneRsaKskKeySize\").hide();\n                $(\"#divDnssecSignZoneRsaZskKeySize\").hide();\n                break;\n\n            case \"EDDSA\":\n                $(\"#divDnssecSignZoneRsaParameters\").hide();\n                $(\"#divDnssecSignZoneEcdsaParameters\").hide();\n                $(\"#divDnssecSignZoneEddsaParameters\").show();\n\n                $(\"#divDnssecSignZoneRsaKskKeySize\").hide();\n                $(\"#divDnssecSignZoneRsaZskKeySize\").hide();\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdDnssecSignZoneKskGeneration]\").on(\"change\", function () {\n        var rdDnssecSignZoneKskGeneration = $(\"input[name=rdDnssecSignZoneKskGeneration]:checked\").val();\n        switch (rdDnssecSignZoneKskGeneration) {\n            case \"Automatic\":\n                if ($(\"input[name=rdDnssecSignZoneAlgorithm]:checked\").val() === \"RSA\")\n                    $(\"#divDnssecSignZoneRsaKskKeySize\").show();\n                else\n                    $(\"#divDnssecSignZoneRsaKskKeySize\").hide();\n\n                $(\"#divDnssecSignZonePemKskPrivateKey\").hide();\n                break;\n\n            case \"UseSpecified\":\n                $(\"#divDnssecSignZoneRsaKskKeySize\").hide();\n                $(\"#divDnssecSignZonePemKskPrivateKey\").show();\n                break;\n        }\n\n        $(\"#txtDnssecSignZonePemKskPrivateKey\").val(\"\");\n    });\n\n    $(\"input[type=radio][name=rdDnssecSignZoneZskGeneration]\").on(\"change\", function () {\n        var rdDnssecSignZoneZskGeneration = $(\"input[name=rdDnssecSignZoneZskGeneration]:checked\").val();\n        switch (rdDnssecSignZoneZskGeneration) {\n            case \"Automatic\":\n                if ($(\"input[name=rdDnssecSignZoneAlgorithm]:checked\").val() === \"RSA\")\n                    $(\"#divDnssecSignZoneRsaZskKeySize\").show();\n                else\n                    $(\"#divDnssecSignZoneRsaZskKeySize\").hide();\n\n                $(\"#divDnssecSignZonePemZskPrivateKey\").hide();\n                $(\"#txtDnssecSignZoneZskAutoRollover\").val(\"30\");\n                break;\n\n            case \"UseSpecified\":\n                $(\"#divDnssecSignZoneRsaZskKeySize\").hide();\n                $(\"#divDnssecSignZonePemZskPrivateKey\").show();\n                $(\"#txtDnssecSignZoneZskAutoRollover\").val(\"0\");\n                break;\n        }\n\n        $(\"#txtDnssecSignZonePemZskPrivateKey\").val(\"\");\n    });\n\n    $(\"input[type=radio][name=rdDnssecSignZoneNxProof]\").on(\"change\", function () {\n        var nxProof = $(\"input[name=rdDnssecSignZoneNxProof]:checked\").val();\n        switch (nxProof) {\n            case \"NSEC\":\n                $(\"#divDnssecSignZoneNSEC3Parameters\").hide();\n                break;\n\n            case \"NSEC3\":\n                $(\"#divDnssecSignZoneNSEC3Parameters\").show();\n                break;\n        }\n    });\n\n    $(\"#optDnssecPropertiesAddKeyKeyType\").on(\"change\", function () {\n        var keyType = $(\"#optDnssecPropertiesAddKeyKeyType\").val();\n        switch (keyType) {\n            case \"ZoneSigningKey\":\n                $(\"#divDnssecPropertiesAddKeyAutomaticRollover\").show();\n\n                if ($(\"input[name=rdDnssecPropertiesKeyGeneration]:checked\").val() === \"Automatic\")\n                    $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val(30);\n                else\n                    $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val(0);\n\n                break;\n\n            default:\n                $(\"#divDnssecPropertiesAddKeyAutomaticRollover\").hide();\n                $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val(0);\n                break;\n        }\n    });\n\n    $(\"#optDnssecPropertiesAddKeyAlgorithm\").on(\"change\", function () {\n        var algorithm = $(\"#optDnssecPropertiesAddKeyAlgorithm\").val();\n        switch (algorithm) {\n            case \"RSA\":\n                $(\"#divDnssecPropertiesAddKeyRsaParameters\").show();\n                $(\"#divDnssecPropertiesAddKeyEcdsaParameters\").hide();\n                $(\"#divDnssecPropertiesAddKeyEddsaParameters\").hide();\n\n                if ($(\"input[name=rdDnssecPropertiesKeyGeneration]:checked\").val() === \"Automatic\")\n                    $(\"#divDnssecPropertiesAddKeyRsaKeySize\").show();\n                else\n                    $(\"#divDnssecPropertiesAddKeyRsaKeySize\").hide();\n\n                break;\n\n            case \"ECDSA\":\n                $(\"#divDnssecPropertiesAddKeyRsaParameters\").hide();\n                $(\"#divDnssecPropertiesAddKeyEcdsaParameters\").show();\n                $(\"#divDnssecPropertiesAddKeyEddsaParameters\").hide();\n\n                $(\"#divDnssecPropertiesAddKeyRsaKeySize\").hide();\n                break;\n\n            case \"EDDSA\":\n                $(\"#divDnssecPropertiesAddKeyRsaParameters\").hide();\n                $(\"#divDnssecPropertiesAddKeyEcdsaParameters\").hide();\n                $(\"#divDnssecPropertiesAddKeyEddsaParameters\").show();\n\n                $(\"#divDnssecPropertiesAddKeyRsaKeySize\").hide();\n                break;\n        }\n    });\n\n    $(\"input[type=radio][name=rdDnssecPropertiesKeyGeneration]\").on(\"change\", function () {\n        var rdDnssecPropertiesKeyGeneration = $(\"input[name=rdDnssecPropertiesKeyGeneration]:checked\").val();\n        switch (rdDnssecPropertiesKeyGeneration) {\n            case \"Automatic\":\n                if ($(\"#optDnssecPropertiesAddKeyAlgorithm\").val() == \"RSA\")\n                    $(\"#divDnssecPropertiesAddKeyRsaKeySize\").show();\n                else\n                    $(\"#divDnssecPropertiesAddKeyRsaKeySize\").hide();\n\n                $(\"#divDnssecPropertiesPemPrivateKey\").hide();\n\n                var keyType = $(\"#optDnssecPropertiesAddKeyKeyType\").val();\n                if (keyType == \"ZoneSigningKey\")\n                    $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val(30);\n                else\n                    $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val(0);\n\n                break;\n\n            case \"UseSpecified\":\n                $(\"#divDnssecPropertiesAddKeyRsaKeySize\").hide();\n                $(\"#divDnssecPropertiesPemPrivateKey\").show();\n                $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val(0);\n                break;\n        }\n\n        $(\"#txtDnssecPropertiesPemPrivateKey\").val(\"\");\n    });\n\n    $(\"input[type=radio][name=rdDnssecPropertiesNxProof]\").on(\"change\", function () {\n        var nxProof = $(\"input[name=rdDnssecPropertiesNxProof]:checked\").val();\n        switch (nxProof) {\n            case \"NSEC\":\n                $(\"#divDnssecPropertiesNSEC3Parameters\").hide();\n                break;\n\n            case \"NSEC3\":\n                $(\"#divDnssecPropertiesNSEC3Parameters\").show();\n                break;\n        }\n    });\n\n    $(\"#chkAddEditRecordDataPtr\").on(\"click\", function () {\n        var addPtrRecord = $(\"#chkAddEditRecordDataPtr\").prop('checked');\n        $(\"#chkAddEditRecordDataCreatePtrZone\").prop('disabled', !addPtrRecord);\n    });\n\n    $(\"#chkAddEditRecordDataTxtSplitText\").on(\"click\", function () {\n        var splitText = $(\"#chkAddEditRecordDataTxtSplitText\").prop(\"checked\");\n        if (!splitText) {\n            var text = $(\"#txtAddEditRecordDataTxt\").val();\n            text = text.replace(/\\n/g, \"\");\n            $(\"#txtAddEditRecordDataTxt\").val(text);\n        }\n    });\n\n    $(\"input[type=radio][name=rdAddEditRecordDataForwarderProtocol]\").on(\"change\", updateAddEditFormForwarderPlaceholder);\n\n    $(\"input[type=radio][name=rdAddEditRecordDataForwarderProxyType]\").on(\"change\", updateAddEditFormForwarderProxyType);\n\n    $(\"#optAddEditRecordDataAppName\").on(\"change\", function () {\n        if (appsList == null)\n            return;\n\n        var appName = $(\"#optAddEditRecordDataAppName\").val();\n        var optClassPaths = \"<option></option>\";\n\n        for (var i = 0; i < appsList.length; i++) {\n            if (appsList[i].name == appName) {\n                for (var j = 0; j < appsList[i].dnsApps.length; j++) {\n                    if (appsList[i].dnsApps[j].isAppRecordRequestHandler)\n                        optClassPaths += \"<option>\" + appsList[i].dnsApps[j].classPath + \"</option>\";\n                }\n\n                break;\n            }\n        }\n\n        $(\"#optAddEditRecordDataClassPath\").html(optClassPaths);\n        $(\"#txtAddEditRecordDataData\").val(\"\");\n    });\n\n    $(\"#optAddEditRecordDataClassPath\").on(\"change\", function () {\n        if (appsList == null)\n            return;\n\n        var appName = $(\"#optAddEditRecordDataAppName\").val();\n        var classPath = $(\"#optAddEditRecordDataClassPath\").val();\n\n        for (var i = 0; i < appsList.length; i++) {\n            if (appsList[i].name == appName) {\n                for (var j = 0; j < appsList[i].dnsApps.length; j++) {\n                    if (appsList[i].dnsApps[j].classPath == classPath) {\n                        $(\"#txtAddEditRecordDataData\").val(appsList[i].dnsApps[j].recordDataTemplate);\n                        return;\n                    }\n                }\n            }\n        }\n\n        $(\"#txtAddEditRecordDataData\").val(\"\");\n    });\n\n    $(\"#optZoneOptionsQuickTsigKeyNames\").on(\"change\", function () {\n        var selectedOption = $(\"#optZoneOptionsQuickTsigKeyNames\").val();\n        switch (selectedOption) {\n            case \"blank\":\n                break;\n\n            case \"none\":\n                $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").val(\"\");\n                break;\n\n            default:\n                var existingList = $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").val();\n\n                if (existingList.indexOf(selectedOption) < 0) {\n                    existingList += selectedOption + \"\\n\";\n                    $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").val(existingList);\n                }\n\n                break;\n        }\n    });\n\n    $(\"#optZonesPerPage\").on(\"change\", function () {\n        localStorage.setItem(\"optZonesPerPage\", $(\"#optZonesPerPage\").val());\n    });\n\n    var optZonesPerPage = localStorage.getItem(\"optZonesPerPage\");\n    if (optZonesPerPage != null)\n        $(\"#optZonesPerPage\").val(optZonesPerPage);\n\n    $(\"#optEditZoneRecordsPerPage\").on(\"change\", function () {\n        localStorage.setItem(\"optEditZoneRecordsPerPage\", $(\"#optEditZoneRecordsPerPage\").val());\n    });\n\n    var optEditZoneRecordsPerPage = localStorage.getItem(\"optEditZoneRecordsPerPage\");\n    if (optEditZoneRecordsPerPage != null)\n        $(\"#optEditZoneRecordsPerPage\").val(optEditZoneRecordsPerPage);\n\n    $(\"#chkEditRecordDataSoaUseSerialDateScheme\").on(\"click\", function () {\n        var useSerialDateScheme = $(\"#chkEditRecordDataSoaUseSerialDateScheme\").prop(\"checked\");\n\n        $(\"#txtEditRecordDataSoaSerial\").prop(\"disabled\", useSerialDateScheme);\n    });\n});\n\nfunction refreshZones(checkDisplay, pageNumber) {\n    if (checkDisplay == null)\n        checkDisplay = false;\n\n    var divViewZones = $(\"#divViewZones\");\n\n    if (checkDisplay) {\n        if (divViewZones.css(\"display\") === \"none\")\n            return;\n\n        if (($(\"#tableZonesBody\").html().length > 0) && !$(\"#mainPanelTabPaneZones\").hasClass(\"active\"))\n            return;\n    }\n\n    if (pageNumber == null) {\n        pageNumber = $(\"#txtZonesPageNumber\").val();\n        if (pageNumber == \"\")\n            pageNumber = 1;\n    }\n\n    var zonesPerPage = Number($(\"#optZonesPerPage\").val());\n    if (zonesPerPage < 1)\n        zonesPerPage = 10;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var divViewZonesLoader = $(\"#divViewZonesLoader\");\n    var divEditZone = $(\"#divEditZone\");\n\n    divViewZones.hide();\n    divEditZone.hide();\n    divViewZonesLoader.show();\n\n    HTTPRequest({\n        url: \"api/zones/list?token=\" + sessionData.token + \"&pageNumber=\" + pageNumber + \"&zonesPerPage=\" + zonesPerPage + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var zones = responseJSON.response.zones;\n            var firstRowNumber = ((responseJSON.response.pageNumber - 1) * zonesPerPage) + 1;\n            var lastRowNumber = firstRowNumber + (zones.length - 1);\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < zones.length; i++) {\n                var id = Math.floor(Math.random() * 10000);\n                var name = zones[i].name;\n\n                if (name === \"\")\n                    name = \".\";\n\n                var type;\n                if (zones[i].internal) {\n                    type = \"<span class=\\\"label label-default\\\">Internal</span>\";\n                }\n                else {\n                    switch (zones[i].type) {\n                        case \"SecondaryForwarder\":\n                            type = \"<span class=\\\"label label-primary\\\">Secondary Forwarder</span>\";\n                            break;\n\n                        case \"SecondaryCatalog\":\n                            type = \"<span class=\\\"label label-primary\\\">Secondary Catalog</span>\";\n                            break;\n\n                        default:\n                            type = \"<span class=\\\"label label-primary\\\">\" + zones[i].type + \"</span>\";\n                            break;\n                    }\n                }\n\n                var soaSerial = zones[i].soaSerial;\n                if (soaSerial == null)\n                    soaSerial = \"&nbsp;\";\n\n                var dnssecStatus = \"\";\n\n                switch (zones[i].dnssecStatus) {\n                    case \"SignedWithNSEC\":\n                    case \"SignedWithNSEC3\":\n                        if (zones[i].hasDnssecPrivateKeys)\n                            dnssecStatus = \"<span class=\\\"label label-primary\\\">DNSSEC</span>\";\n                        else\n                            dnssecStatus = \"<span class=\\\"label label-default\\\">DNSSEC</span>\";\n\n                        break;\n                }\n\n                var status = \"\";\n\n                if (zones[i].disabled)\n                    status = \"<span id=\\\"tdZoneStatus\" + id + \"\\\" class=\\\"label label-default\\\">Disabled</span>\";\n                else if (zones[i].isExpired)\n                    status = \"<span id=\\\"tdZoneStatus\" + id + \"\\\" class=\\\"label label-danger\\\">Expired</span>\";\n                else if (zones[i].validationFailed)\n                    status = \"<span id=\\\"tdZoneStatus\" + id + \"\\\" class=\\\"label label-danger\\\">Validation Failed</span>\";\n                else if (zones[i].syncFailed)\n                    status = \"<span id=\\\"tdZoneStatus\" + id + \"\\\" class=\\\"label label-warning\\\">Sync Failed</span>\";\n                else if (zones[i].notifyFailed)\n                    status = \"<span id=\\\"tdZoneStatus\" + id + \"\\\" class=\\\"label label-warning\\\">Notify Failed</span>\";\n                else\n                    status = \"<span id=\\\"tdZoneStatus\" + id + \"\\\" class=\\\"label label-success\\\">Enabled</span>\";\n\n                var expiry = zones[i].expiry;\n                if (expiry == null)\n                    expiry = \"&nbsp;\";\n                else\n                    expiry = moment(expiry).local().format(\"YYYY-MM-DD HH:mm\");\n\n                var lastModified = zones[i].lastModified;\n                if (lastModified == null)\n                    lastModified = \"&nbsp;\";\n                else\n                    lastModified = moment(lastModified).local().format(\"YYYY-MM-DD HH:mm\");\n\n                var isReadOnlyZone = zones[i].internal;\n\n                var showResyncMenu;\n\n                switch (zones[i].type) {\n                    case \"Secondary\":\n                    case \"SecondaryForwarder\":\n                    case \"SecondaryCatalog\":\n                    case \"Stub\":\n                        showResyncMenu = true;\n                        break;\n\n                    default:\n                        showResyncMenu = false;\n                        break;\n                }\n\n                var hideOptionsMenu;\n\n                switch (zones[i].type) {\n                    case \"Primary\":\n                        hideOptionsMenu = zones[i].internal;\n                        break;\n\n                    case \"Secondary\":\n                    case \"SecondaryForwarder\":\n                    case \"SecondaryCatalog\":\n                    case \"Stub\":\n                    case \"Forwarder\":\n                    case \"Catalog\":\n                        hideOptionsMenu = false;\n                        break;\n\n                    default:\n                        hideOptionsMenu = true;\n                        break;\n                }\n\n                var nameTags;\n\n                if (zones[i].catalog != null) {\n                    nameTags = \"<div><span id=\\\"tagZoneCatalogName\" + id + \"\\\" class=\\\"label label-default\\\">\" + htmlEncode(zones[i].catalog) + \"</span></div>\";\n                }\n                else {\n                    switch (zones[i].type) {\n                        case \"Catalog\":\n                        case \"SecondaryCatalog\":\n                            nameTags = \"<div><span id=\\\"tagZoneCatalogName\" + id + \"\\\" class=\\\"label label-info\\\">\" + htmlEncode(name) + \"</span></div>\";\n                            break;\n\n                        default:\n                            nameTags = \"<div><span id=\\\"tagZoneCatalogName\" + id + \"\\\" class=\\\"label label-default\\\" style=\\\"display: none;\\\"></span></div>\";\n                            break;\n                    }\n                }\n\n                tableHtmlRows += \"<tr id=\\\"trZone\" + id + \"\\\"><td>\" + (firstRowNumber + i) + \"</td>\";\n\n                if (zones[i].nameIdn == null)\n                    tableHtmlRows += \"<td style=\\\"word-break: break-word; max-width: 390px;\\\"><a href=\\\"#\\\" style=\\\"font-weight: bold;\\\" onclick=\\\"showEditZone('\" + name + \"'); return false;\\\">\" + htmlEncode(name === \".\" ? \"<root>\" : name) + \"</a>\" + nameTags + \"</td>\";\n                else\n                    tableHtmlRows += \"<td style=\\\"word-break: break-word; max-width: 390px;\\\"><a href=\\\"#\\\" style=\\\"font-weight: bold;\\\" onclick=\\\"showEditZone('\" + name + \"'); return false;\\\">\" + htmlEncode(zones[i].nameIdn + \" (\" + name + \")\") + \"</a>\" + nameTags + \"</td>\";\n\n                tableHtmlRows += \"<td>\" + type + \"</td>\";\n                tableHtmlRows += \"<td>\" + dnssecStatus + \"</td>\";\n                tableHtmlRows += \"<td>\" + status + \"</td>\";\n                tableHtmlRows += \"<td>\" + soaSerial + \"</td>\";\n                tableHtmlRows += \"<td>\" + expiry + \"</td>\";\n                tableHtmlRows += \"<td>\" + lastModified + \"</td>\";\n\n                tableHtmlRows += \"<td align=\\\"right\\\"><div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnZoneRowOption\" + id + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"showEditZone('\" + name + \"'); return false;\\\">\" + (isReadOnlyZone ? \"View\" : \"Edit\") + \" Zone</a></li>\";\n\n                if (!zones[i].internal) {\n                    tableHtmlRows += \"<li id=\\\"mnuEnableZone\" + id + \"\\\"\" + (zones[i].disabled ? \"\" : \" style=\\\"display: none;\\\"\") + \"><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-zone=\\\"\" + htmlEncode(name) + \"\\\" onclick=\\\"enableZoneMenu(this); return false;\\\">Enable</a></li>\";\n                    tableHtmlRows += \"<li id=\\\"mnuDisableZone\" + id + \"\\\"\" + (!zones[i].disabled ? \"\" : \" style=\\\"display: none;\\\"\") + \"><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-zone=\\\"\" + htmlEncode(name) + \"\\\" onclick=\\\"disableZoneMenu(this); return false;\\\">Disable</a></li>\";\n                }\n\n                if (showResyncMenu) {\n                    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-zone=\\\"\" + htmlEncode(name) + \"\\\" data-zone-type=\\\"\" + zones[i].type + \"\\\" onclick=\\\"resyncZoneMenu(this); return false;\\\">Resync</a></li>\";\n                }\n\n                switch (zones[i].type) {\n                    case \"Primary\":\n                    case \"Forwarder\":\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"showImportZoneModal('\" + name + \"'); return false;\\\">Import Zone</a></li>\";\n                        break;\n                }\n\n                switch (zones[i].type) {\n                    case \"Primary\":\n                    case \"Forwarder\":\n                    case \"Secondary\":\n                    case \"SecondaryForwarder\":\n                    case \"SecondaryCatalog\":\n                    case \"Catalog\":\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"exportZone('\" + name + \"'); return false;\\\">Export Zone</a></li>\";\n                        break;\n                }\n\n                switch (zones[i].type) {\n                    case \"Primary\":\n                    case \"Secondary\":\n                    case \"SecondaryForwarder\":\n                    case \"Forwarder\":\n                    case \"SecondaryCatalog\":\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"showConvertZoneModal('\" + name + \"', '\" + zones[i].type + \"'); return false;\\\">Convert Zone</a></li>\";\n                        break;\n                }\n\n                switch (zones[i].type) {\n                    case \"Primary\":\n                    case \"Forwarder\":\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"showCloneZoneModal('\" + name + \"'); return false;\\\">Clone Zone</a></li>\";\n                        break;\n                }\n\n                if (!zones[i].internal) {\n                    tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"showZonePermissionsModal('\" + name + \"'); return false;\\\">Permissions</a></li>\";\n                }\n\n                if (!hideOptionsMenu) {\n                    tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"$('#btnSaveZoneOptions').attr('data-zones-row-id', \" + id + \"); showZoneOptionsModal('\" + name + \"'); return false;\\\">Zone Options</a></li>\";\n                }\n\n                if (!zones[i].internal) {\n                    tableHtmlRows += \"<li role=\\\"separator\\\" class=\\\"divider\\\"></li>\";\n                    tableHtmlRows += \"<li><a href=\\\"#\\\" data-id=\\\"\" + id + \"\\\" data-zone=\\\"\" + htmlEncode(name) + \"\\\" onclick=\\\"deleteZoneMenu(this); return false;\\\">Delete Zone</a></li>\";\n                }\n\n                tableHtmlRows += \"</ul></div></td></tr>\";\n            }\n\n            var paginationHtml = \"\";\n\n            if (responseJSON.response.pageNumber > 1) {\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"First\\\" onClick=\\\"refreshZones(false, 1); return false;\\\"><span aria-hidden=\\\"true\\\">&laquo;</span></a></li>\";\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Previous\\\" onClick=\\\"refreshZones(false, \" + (responseJSON.response.pageNumber - 1) + \"); return false;\\\"><span aria-hidden=\\\"true\\\">&lsaquo;</span></a></li>\";\n            }\n\n            var pageStart = responseJSON.response.pageNumber - 5;\n            if (pageStart < 1)\n                pageStart = 1;\n\n            var pageEnd = pageStart + 9;\n            if (pageEnd > responseJSON.response.totalPages) {\n                var endDiff = pageEnd - responseJSON.response.totalPages;\n                pageEnd = responseJSON.response.totalPages;\n\n                pageStart -= endDiff;\n                if (pageStart < 1)\n                    pageStart = 1;\n            }\n\n            for (var i = pageStart; i <= pageEnd; i++) {\n                if (i == responseJSON.response.pageNumber)\n                    paginationHtml += \"<li class=\\\"active\\\"><a href=\\\"#\\\" onClick=\\\"refreshZones(false, \" + i + \"); return false;\\\">\" + i + \"</a></li>\";\n                else\n                    paginationHtml += \"<li><a href=\\\"#\\\" onClick=\\\"refreshZones(false, \" + i + \"); return false;\\\">\" + i + \"</a></li>\";\n            }\n\n            if (responseJSON.response.pageNumber < responseJSON.response.totalPages) {\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Next\\\" onClick=\\\"refreshZones(false, \" + (responseJSON.response.pageNumber + 1) + \"); return false;\\\"><span aria-hidden=\\\"true\\\">&rsaquo;</span></a></li>\";\n                paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Last\\\" onClick=\\\"refreshZones(false, -1); return false;\\\"><span aria-hidden=\\\"true\\\">&raquo;</span></a></li>\";\n            }\n\n            var statusHtml;\n\n            if (responseJSON.response.zones.length > 0)\n                statusHtml = firstRowNumber + \"-\" + lastRowNumber + \" (\" + responseJSON.response.zones.length + \") of \" + responseJSON.response.totalZones + \" zones (page \" + responseJSON.response.pageNumber + \" of \" + responseJSON.response.totalPages + \")\";\n            else\n                statusHtml = \"0 zones\";\n\n            $(\"#txtZonesPageNumber\").val(responseJSON.response.pageNumber);\n            $(\"#tableZonesBody\").html(tableHtmlRows);\n\n            $(\"#tableZonesTopStatus\").html(statusHtml);\n            $(\"#tableZonesTopPagination\").html(paginationHtml);\n\n            $(\"#tableZonesFooterStatus\").html(statusHtml);\n            $(\"#tableZonesFooterPagination\").html(paginationHtml);\n\n            divViewZonesLoader.hide();\n            divViewZones.show();\n        },\n        error: function () {\n            divViewZonesLoader.hide();\n            divViewZones.show();\n        },\n        invalidToken: function () {\n            divViewZonesLoader.hide();\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divViewZonesLoader\n    });\n}\n\nfunction enableZoneMenu(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var zone = mnuItem.attr(\"data-zone\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnZoneRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/zones/enable?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n\n            $(\"#mnuEnableZone\" + id).hide();\n            $(\"#mnuDisableZone\" + id).show();\n            $(\"#tdZoneStatus\" + id).attr(\"class\", \"label label-success\");\n            $(\"#tdZoneStatus\" + id).html(\"Enabled\");\n\n            showAlert(\"success\", \"Zone Enabled!\", \"Zone '\" + zone + \"' was enabled successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction enableZone(objBtn) {\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/enable?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n\n            $(\"#btnEnableZoneEditZone\").hide();\n            $(\"#btnDisableZoneEditZone\").show();\n            $(\"#titleEditZoneStatus\").attr(\"class\", \"label label-success\");\n            $(\"#titleEditZoneStatus\").html(\"Enabled\");\n\n            showAlert(\"success\", \"Zone Enabled!\", \"Zone '\" + zone + \"' was enabled successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction disableZoneMenu(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var zone = mnuItem.attr(\"data-zone\");\n\n    if (!confirm(\"Are you sure you want to disable the zone '\" + zone + \"'?\"))\n        return;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnZoneRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/zones/disable?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n\n            $(\"#mnuEnableZone\" + id).show();\n            $(\"#mnuDisableZone\" + id).hide();\n            $(\"#tdZoneStatus\" + id).attr(\"class\", \"label label-default\");\n            $(\"#tdZoneStatus\" + id).html(\"Disabled\");\n\n            showAlert(\"success\", \"Zone Disabled!\", \"Zone '\" + zone + \"' was disabled successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction disableZone(objBtn) {\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n\n    if (!confirm(\"Are you sure you want to disable the zone '\" + zone + \"'?\"))\n        return;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/disable?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n\n            $(\"#btnEnableZoneEditZone\").show();\n            $(\"#btnDisableZoneEditZone\").hide();\n            $(\"#titleEditZoneStatus\").attr(\"class\", \"label label-default\");\n            $(\"#titleEditZoneStatus\").html(\"Disabled\");\n\n            showAlert(\"success\", \"Zone Disabled!\", \"Zone '\" + zone + \"' was disabled successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteZoneMenu(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var zone = mnuItem.attr(\"data-zone\");\n\n    if (!confirm(\"Are you sure you want to permanently delete the zone '\" + zone + \"' and all its records?\"))\n        return;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnZoneRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/zones/delete?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshZones();\n\n            showAlert(\"success\", \"Zone Deleted!\", \"Zone '\" + zone + \"' was deleted successfully.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteZone(objBtn) {\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n\n    if (!confirm(\"Are you sure you want to permanently delete the zone '\" + zone + \"' and all its records?\"))\n        return;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/delete?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            refreshZones();\n\n            showAlert(\"success\", \"Zone Deleted!\", \"Zone '\" + zone + \"' was deleted successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction showImportZoneModal(zone) {\n    $(\"#lblImportZoneName\").text(zone);\n    $(\"#divImportZoneAlert\").html(\"\");\n\n    $(\"#rdImportZoneTypeFile\").prop(\"checked\", true);\n    $(\"#chkImportZoneOverwrite\").prop(\"checked\", true)\n    $(\"#chkImportZoneOverwriteSoaSerial\").prop(\"checked\", false)\n\n    $(\"#divImportZoneFile\").show();\n    $(\"#fileImportZone\").val(\"\");\n\n    $(\"#divImportZoneTextEditor\").hide();\n    $(\"#txtImportZoneText\").val(\"\");\n\n    $(\"#btnImportZone\").button(\"reset\");\n\n    $(\"#modalImportZone\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtImportZoneText\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction importZone() {\n    var divImportZoneAlert = $(\"#divImportZoneAlert\");\n\n    var zone = $(\"#lblImportZoneName\").text();\n    var importType = $(\"input[name=rdImportZoneType]:checked\").val();\n    var overwrite = $(\"#chkImportZoneOverwrite\").prop(\"checked\");\n    var overwriteSoaSerial = $(\"#chkImportZoneOverwriteSoaSerial\").prop(\"checked\");\n\n    var formData;\n    var contentType;\n\n    switch (importType) {\n        case \"File\":\n            var fileImportZone = $(\"#fileImportZone\");\n\n            if (fileImportZone[0].files.length === 0) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a zone file to import.\", divImportZoneAlert);\n                fileImportZone.trigger(\"focus\");\n                return;\n            }\n\n            formData = new FormData();\n            formData.append(\"fileImportZone\", fileImportZone[0].files[0]);\n            contentType = false;\n            break;\n\n        default:\n            formData = $(\"#txtImportZoneText\").val();\n            contentType = \"text/plain\";\n            break;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnImportZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/import?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&overwrite=\" + overwrite + \"&overwriteSoaSerial=\" + overwriteSoaSerial + \"&node=\" + encodeURIComponent(node),\n        method: \"POST\",\n        data: formData,\n        contentType: contentType,\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalImportZone\").modal(\"hide\");\n\n            if ($(\"#divEditZone\").is(\":visible\"))\n                showEditZone(zone);\n\n            showAlert(\"success\", \"Zone Imported!\", \"The zone file was imported successfully.\");\n        },\n        error: function () {\n            btn.button('reset');\n        },\n        invalidToken: function () {\n            $(\"#modalImportZone\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divImportZoneAlert\n    });\n}\n\nfunction exportZone(zone) {\n    var node = $(\"#optZonesClusterNode\").val();\n\n    window.open(\"api/zones/export?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node), \"_blank\");\n\n    showAlert(\"success\", \"Zone Exported!\", \"Zone file was exported successfully.\");\n}\n\nfunction showCloneZoneModal(sourceZone) {\n    $(\"#lblCloneZoneZoneName\").text(sourceZone === \".\" ? \"<root>\" : sourceZone);\n\n    $(\"#divCloneZoneAlert\").html(\"\");\n    $(\"#txtCloneZoneSourceZoneName\").val(sourceZone);\n    $(\"#txtCloneZoneZoneName\").val(\"\");\n\n    $(\"#modalCloneZone\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtCloneZoneZoneName\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction cloneZone(objBtn) {\n    var divCloneZoneAlert = $(\"#divCloneZoneAlert\");\n\n    var sourceZone = $(\"#txtCloneZoneSourceZoneName\").val();\n\n    var zone = $(\"#txtCloneZoneZoneName\").val();\n    if ((zone == null) || (zone === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a domain name for the new zone.\", divCloneZoneAlert);\n        $(\"#txtCloneZoneZoneName\").trigger(\"focus\");\n        return;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/clone?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&sourceZone=\" + encodeURIComponent(sourceZone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalCloneZone\").modal(\"hide\");\n\n            if ($(\"#divEditZone\").is(\":hidden\"))\n                refreshZones();\n\n            showAlert(\"success\", \"Zone Cloned!\", \"Zone was cloned from successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalCloneZone\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divCloneZoneAlert\n    });\n}\n\nfunction showConvertZoneModal(zone, type) {\n    var lblConvertZoneZoneName = $(\"#lblConvertZoneZoneName\");\n\n    lblConvertZoneZoneName.text(zone === \".\" ? \"<root>\" : zone);\n    lblConvertZoneZoneName.attr(\"data-zone\", zone);\n\n    $(\"#divConvertZoneAlert\").html(\"\");\n\n    switch (type) {\n        case \"Primary\":\n            $(\"#rdConvertZoneToTypePrimary\").attr(\"disabled\", true);\n            $(\"#rdConvertZoneToTypeForwarder\").attr(\"disabled\", false);\n            $(\"#rdConvertZoneToTypeCatalog\").attr(\"disabled\", true);\n\n            $(\"#rdConvertZoneToTypeForwarder\").prop(\"checked\", true);\n            break;\n\n        case \"Secondary\":\n        case \"SecondaryForwarder\":\n            $(\"#rdConvertZoneToTypePrimary\").attr(\"disabled\", false);\n            $(\"#rdConvertZoneToTypeForwarder\").attr(\"disabled\", false);\n            $(\"#rdConvertZoneToTypeCatalog\").attr(\"disabled\", true);\n\n            $(\"#rdConvertZoneToTypePrimary\").prop(\"checked\", true);\n            break;\n\n        case \"Forwarder\":\n            $(\"#rdConvertZoneToTypePrimary\").attr(\"disabled\", false);\n            $(\"#rdConvertZoneToTypeForwarder\").attr(\"disabled\", true);\n            $(\"#rdConvertZoneToTypeCatalog\").attr(\"disabled\", true);\n\n            $(\"#rdConvertZoneToTypePrimary\").prop(\"checked\", true);\n            break;\n\n        case \"SecondaryCatalog\":\n            $(\"#rdConvertZoneToTypePrimary\").attr(\"disabled\", true);\n            $(\"#rdConvertZoneToTypeForwarder\").attr(\"disabled\", true);\n            $(\"#rdConvertZoneToTypeCatalog\").attr(\"disabled\", false);\n\n            $(\"#rdConvertZoneToTypeCatalog\").prop(\"checked\", true);\n            break;\n\n        default:\n            $(\"#rdConvertZoneToTypePrimary\").attr(\"disabled\", true);\n            $(\"#rdConvertZoneToTypeForwarder\").attr(\"disabled\", true);\n            $(\"#rdConvertZoneToTypeCatalog\").attr(\"disabled\", true);\n\n            $(\"#rdConvertZoneToTypePrimary\").prop(\"checked\", false);\n            $(\"#rdConvertZoneToTypeForwarder\").prop(\"checked\", false);\n            $(\"#rdConvertZoneToTypeCatalog\").prop(\"checked\", false);\n            break;\n    }\n\n    $(\"#modalConvertZone\").modal(\"show\");\n}\n\nfunction convertZone(objBtn) {\n    var divConvertZoneAlert = $(\"#divConvertZoneAlert\");\n\n    var zone = $(\"#lblConvertZoneZoneName\").attr(\"data-zone\");\n    var type = $(\"input[name=rdConvertZoneToType]:checked\").val();\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/convert?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&type=\" + type + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalConvertZone\").modal(\"hide\");\n\n            if ($(\"#divEditZone\").is(\":visible\"))\n                showEditZone(zone);\n            else\n                refreshZones();\n\n            showAlert(\"success\", \"Zone Converted!\", \"The zone was converted successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalConvertZone\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divConvertZoneAlert\n    });\n}\n\nfunction addZoneOptionsDynamicUpdatesSecurityPolicyRow(id, tsigKeyName, domain, allowedTypes) {\n    var tbodyDynamicUpdateSecurityPolicy = $(\"#tbodyDynamicUpdateSecurityPolicy\");\n\n    if (id == null) {\n        id = Math.floor(Math.random() * 10000);\n\n        if (tbodyDynamicUpdateSecurityPolicy.is(\":empty\")) {\n            tsigKeyName = null;\n            domain = $(\"#lblZoneOptionsZoneName\").attr(\"data-zone\");\n            allowedTypes = 'A,AAAA'.split(',');\n        }\n    }\n\n    var tableHtmlRow = \"<tr id=\\\"trDynamicUpdateSecurityPolicyRow\" + id + \"\\\"><td style=\\\"word-wrap: anywhere;\\\"><select class=\\\"form-control\\\">\";\n\n    if (tsigKeyName != null)\n        tableHtmlRow += \"<option selected>\" + htmlEncode(tsigKeyName) + \"</option>\";\n\n    for (var i = 0; i < zoneOptionsAvailableTsigKeyNames.length; i++) {\n        if (zoneOptionsAvailableTsigKeyNames[i] === tsigKeyName)\n            continue;\n\n        tableHtmlRow += \"<option>\" + htmlEncode(zoneOptionsAvailableTsigKeyNames[i]) + \"</option>\";\n    }\n\n    tableHtmlRow += \"</select></td>\";\n    tableHtmlRow += \"<td><input class=\\\"form-control\\\" type=\\\"text\\\" value=\\\"\" + htmlEncode(domain) + \"\\\"></td>\";\n    tableHtmlRow += \"<td><input class=\\\"form-control\\\" type=\\\"text\\\" value=\\\"\";\n\n    if (allowedTypes != null) {\n        for (var i = 0; i < allowedTypes.length; i++) {\n            if (i == 0)\n                tableHtmlRow += htmlEncode(allowedTypes[i]);\n            else\n                tableHtmlRow += \", \" + htmlEncode(allowedTypes[i]);\n        }\n    }\n\n    tableHtmlRow += \"\\\"></td>\";\n    tableHtmlRow += \"<td align=\\\"right\\\"><button type=\\\"button\\\" class=\\\"btn btn-warning\\\" style=\\\"padding: 5px 7px;\\\" onclick=\\\"$('#trDynamicUpdateSecurityPolicyRow\" + id + \"').remove();\\\">Remove</button></td></tr>\";\n\n    tbodyDynamicUpdateSecurityPolicy.append(tableHtmlRow);\n}\n\nfunction showZoneOptionsModal(zone) {\n    var divZoneOptionsAlert = $(\"#divZoneOptionsAlert\");\n    var divZoneOptionsLoader = $(\"#divZoneOptionsLoader\");\n    var divZoneOptions = $(\"#divZoneOptions\");\n\n    $(\"#lblZoneOptionsZoneName\").text(zone === \".\" ? \"<root>\" : zone);\n    $(\"#lblZoneOptionsZoneName\").attr(\"data-zone\", zone);\n    divZoneOptionsLoader.show();\n    divZoneOptions.hide();\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    $(\"#modalZoneOptions\").modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/zones/options/get?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&includeAvailableCatalogZoneNames=true&includeAvailableTsigKeyNames=true\" + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#optZoneOptionsCatalogZoneName\").html(\"\");\n\n            $(\"#lblZoneOptionsPrimaryNameServerAddresses\").text(\"Primary Name Server Addresses (Optional)\");\n            $(\"#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.\");\n            $(\"#txtZoneOptionsPrimaryNameServerAddresses\").val(\"\");\n            $(\"#rdPrimaryZoneTransferProtocolTcp\").prop(\"checked\", true);\n            $(\"#optZoneOptionsPrimaryZoneTransferTsigKeyName\").val(\"\");\n            $(\"#chkZoneOptionsValidateZone\").prop(\"checked\", false);\n\n            $(\"#tabListZoneOptionsGeneral\").hide();\n\n            $(\"#divZoneOptionsCatalogNotifyFailedNameServers\").hide();\n\n            $(\"#rdDynamicUpdateDeny\").prop(\"checked\", true);\n            $(\"#txtDynamicUpdateNetworkACL\").val(\"\");\n            $(\"#tbodyDynamicUpdateSecurityPolicy\").html(\"\");\n\n            $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", true);\n            $(\"#txtZoneTransferNetworkACL\").prop(\"disabled\", true);\n            $(\"#txtZoneNotifyNameServers\").prop(\"disabled\", true);\n            $(\"#txtZoneNotifySecondaryCatalogNameServers\").prop(\"disabled\", true);\n            $(\"#txtDynamicUpdateNetworkACL\").prop(\"disabled\", true);\n\n            $(\"#lblZoneOptionsZoneName\").attr(\"data-zone-type\", responseJSON.response.type);\n\n            //catalog zone\n            switch (responseJSON.response.type) {\n                case \"Primary\":\n                case \"Forwarder\":\n                    if (responseJSON.response.availableCatalogZoneNames.length > 0) {\n                        loadCatalogZoneNamesFrom(responseJSON.response.availableCatalogZoneNames, $(\"#optZoneOptionsCatalogZoneName\"), responseJSON.response.catalog);\n                        $(\"#optZoneOptionsCatalogZoneName\").prop(\"disabled\", false);\n\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogQueryAccess);\n                        $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"checked\", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogZoneTransfer);\n                        $(\"#chkZoneOptionsCatalogOverrideNotify\").prop(\"checked\", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogNotify);\n\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", (responseJSON.response.catalog == null));\n                        $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"disabled\", (responseJSON.response.catalog == null));\n                        $(\"#chkZoneOptionsCatalogOverrideNotify\").prop(\"disabled\", (responseJSON.response.catalog == null));\n\n                        $(\"#divZoneOptionsCatalogOverrideZoneTransfer\").show();\n                        $(\"#divZoneOptionsCatalogOverrideNotify\").show();\n\n                        $(\"#divZoneOptionsCatalogOverrideOptions\").show();\n                        $(\"#divZoneOptionsGeneralCatalogZone\").show();\n                        $(\"#tabListZoneOptionsGeneral\").show();\n                    } else {\n                        $(\"#divZoneOptionsGeneralCatalogZone\").hide();\n                    }\n                    break;\n\n                case \"Stub\":\n                    if ((responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember) {\n                        $(\"#optZoneOptionsCatalogZoneName\").html(\"<option selected>\" + htmlEncode(responseJSON.response.catalog) + \"</option>\");\n                        $(\"#optZoneOptionsCatalogZoneName\").prop(\"disabled\", true);\n\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\", responseJSON.response.overrideCatalogQueryAccess);\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", true);\n\n                        $(\"#divZoneOptionsCatalogOverrideZoneTransfer\").hide();\n                        $(\"#divZoneOptionsCatalogOverrideNotify\").hide();\n\n                        $(\"#divZoneOptionsCatalogOverrideOptions\").show();\n                        $(\"#divZoneOptionsGeneralCatalogZone\").show();\n                        $(\"#tabListZoneOptionsGeneral\").show();\n                    } else {\n                        if (responseJSON.response.availableCatalogZoneNames.length > 0) {\n                            loadCatalogZoneNamesFrom(responseJSON.response.availableCatalogZoneNames, $(\"#optZoneOptionsCatalogZoneName\"), responseJSON.response.catalog);\n                            $(\"#optZoneOptionsCatalogZoneName\").prop(\"disabled\", false);\n\n                            $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogQueryAccess);\n                            $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", (responseJSON.response.catalog == null));\n\n                            $(\"#divZoneOptionsCatalogOverrideZoneTransfer\").hide();\n                            $(\"#divZoneOptionsCatalogOverrideNotify\").hide();\n\n                            $(\"#divZoneOptionsCatalogOverrideOptions\").show();\n                            $(\"#divZoneOptionsGeneralCatalogZone\").show();\n                            $(\"#tabListZoneOptionsGeneral\").show();\n                        } else {\n                            $(\"#divZoneOptionsGeneralCatalogZone\").hide();\n                        }\n                    }\n\n                    break;\n\n                case \"Secondary\":\n                    if ((responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember) {\n                        $(\"#optZoneOptionsCatalogZoneName\").html(\"<option selected>\" + htmlEncode(responseJSON.response.catalog) + \"</option>\");\n                        $(\"#optZoneOptionsCatalogZoneName\").prop(\"disabled\", true);\n\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\", responseJSON.response.overrideCatalogQueryAccess);\n                        $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"checked\", responseJSON.response.overrideCatalogZoneTransfer);\n\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", true);\n                        $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"disabled\", true);\n\n                        $(\"#divZoneOptionsCatalogOverrideZoneTransfer\").show();\n                        $(\"#divZoneOptionsCatalogOverrideNotify\").hide();\n\n                        $(\"#divZoneOptionsCatalogOverrideOptions\").show();\n                        $(\"#divZoneOptionsGeneralCatalogZone\").show();\n                        $(\"#tabListZoneOptionsGeneral\").show();\n                    } else {\n                        if (responseJSON.response.availableCatalogZoneNames.length > 0) {\n                            loadCatalogZoneNamesFrom(responseJSON.response.availableCatalogZoneNames, $(\"#optZoneOptionsCatalogZoneName\"), responseJSON.response.catalog);\n                            $(\"#optZoneOptionsCatalogZoneName\").prop(\"disabled\", false);\n\n                            $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogQueryAccess);\n                            $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"checked\", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogZoneTransfer);\n\n                            $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", (responseJSON.response.catalog == null));\n                            $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"disabled\", (responseJSON.response.catalog == null));\n\n                            $(\"#divZoneOptionsCatalogOverrideZoneTransfer\").show();\n                            $(\"#divZoneOptionsCatalogOverrideNotify\").hide();\n\n                            $(\"#divZoneOptionsCatalogOverrideOptions\").show();\n                            $(\"#divZoneOptionsGeneralCatalogZone\").show();\n                            $(\"#tabListZoneOptionsGeneral\").show();\n                        }\n                        else {\n                            $(\"#divZoneOptionsGeneralCatalogZone\").hide();\n                        }\n                    }\n                    break;\n\n                case \"SecondaryForwarder\":\n                    if (responseJSON.response.catalog != null) {\n                        $(\"#optZoneOptionsCatalogZoneName\").html(\"<option selected>\" + htmlEncode(responseJSON.response.catalog) + \"</option>\");\n                        $(\"#optZoneOptionsCatalogZoneName\").prop(\"disabled\", true);\n\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\", responseJSON.response.overrideCatalogQueryAccess);\n                        $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"disabled\", true);\n\n                        $(\"#divZoneOptionsCatalogOverrideZoneTransfer\").hide();\n                        $(\"#divZoneOptionsCatalogOverrideNotify\").hide();\n\n                        $(\"#divZoneOptionsCatalogOverrideOptions\").show();\n                        $(\"#divZoneOptionsGeneralCatalogZone\").show();\n                        $(\"#tabListZoneOptionsGeneral\").show();\n                    } else {\n                        $(\"#divZoneOptionsGeneralCatalogZone\").hide();\n                    }\n                    break;\n\n                default:\n                    $(\"#divZoneOptionsGeneralCatalogZone\").hide();\n                    break;\n            }\n\n            //primary server\n            switch (responseJSON.response.type) {\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                    {\n                        var value = \"\";\n\n                        for (var i = 0; i < responseJSON.response.primaryNameServerAddresses.length; i++)\n                            value += responseJSON.response.primaryNameServerAddresses[i] + \"\\r\\n\";\n\n                        $(\"#txtZoneOptionsPrimaryNameServerAddresses\").val(value);\n                    }\n\n                    switch (responseJSON.response.primaryZoneTransferProtocol) {\n                        case \"Tls\":\n                            $(\"#rdPrimaryZoneTransferProtocolTls\").prop(\"checked\", true);\n                            break;\n\n                        case \"Quic\":\n                            $(\"#rdPrimaryZoneTransferProtocolQuic\").prop(\"checked\", true);\n                            break;\n\n                        case \"Tcp\":\n                        default:\n                            $(\"#rdPrimaryZoneTransferProtocolTcp\").prop(\"checked\", true);\n                            break;\n                    }\n\n                    loadTsigKeyNamesFrom(responseJSON.response.availableTsigKeyNames, $(\"#optZoneOptionsPrimaryZoneTransferTsigKeyName\"), responseJSON.response.primaryZoneTransferTsigKeyName);\n\n                    if (responseJSON.response.type == \"Secondary\") {\n                        $(\"#chkZoneOptionsValidateZone\").prop(\"checked\", responseJSON.response.validateZone);\n                        $(\"#divZoneOptionsPrimaryServerValidateZone\").show();\n                    }\n                    else {\n                        $(\"#divZoneOptionsPrimaryServerValidateZone\").hide();\n                    }\n\n                    switch (responseJSON.response.type) {\n                        case \"SecondaryForwarder\":\n                        case \"SecondaryCatalog\":\n                            $(\"#lblZoneOptionsPrimaryNameServerAddresses\").text(\"Primary Name Server Addresses\");\n                            $(\"#divZoneOptionsPrimaryNameServerAddressesInfo\").text(\"Enter the primary name server addresses to sync the zone from.\");\n                            break;\n                    }\n\n                    $(\"#divZoneOptionsPrimaryServerZoneTransferProtocol\").show();\n                    $(\"#divZoneOptionsPrimaryServerZoneTransferTsigKeyName\").show();\n\n                    var disableControls = (responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember;\n\n                    $(\"#txtZoneOptionsPrimaryNameServerAddresses\").prop(\"disabled\", disableControls);\n                    $(\"#rdPrimaryZoneTransferProtocolTcp\").prop(\"disabled\", disableControls);\n                    $(\"#rdPrimaryZoneTransferProtocolTls\").prop(\"disabled\", disableControls);\n                    $(\"#rdPrimaryZoneTransferProtocolQuic\").prop(\"disabled\", disableControls);\n                    $(\"#optZoneOptionsPrimaryZoneTransferTsigKeyName\").prop(\"disabled\", disableControls);\n                    $(\"#chkZoneOptionsValidateZone\").prop(\"disabled\", disableControls);\n\n                    switch (responseJSON.response.type) {\n                        case \"Secondary\":\n                        case \"SecondaryForwarder\":\n                            if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogPrimaryNameServers) {\n                                $(\"#divZoneOptionsGeneralPrimaryServer\").show();\n                                $(\"#tabListZoneOptionsGeneral\").show();\n                            } else {\n                                $(\"#divZoneOptionsGeneralPrimaryServer\").hide();\n                            }\n\n                            break;\n\n                        default:\n                            $(\"#divZoneOptionsGeneralPrimaryServer\").show();\n                            $(\"#tabListZoneOptionsGeneral\").show();\n                            break;\n                    }\n\n                    break;\n\n                case \"Stub\":\n                    {\n                        var value = \"\";\n\n                        for (var i = 0; i < responseJSON.response.primaryNameServerAddresses.length; i++)\n                            value += responseJSON.response.primaryNameServerAddresses[i] + \"\\r\\n\";\n\n                        $(\"#txtZoneOptionsPrimaryNameServerAddresses\").val(value);\n                    }\n\n                    $(\"#txtZoneOptionsPrimaryNameServerAddresses\").prop(\"disabled\", (responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember);\n\n                    $(\"#divZoneOptionsPrimaryServerZoneTransferProtocol\").hide();\n                    $(\"#divZoneOptionsPrimaryServerZoneTransferTsigKeyName\").hide();\n                    $(\"#divZoneOptionsPrimaryServerValidateZone\").hide();\n                    $(\"#divZoneOptionsGeneralPrimaryServer\").show();\n                    $(\"#tabListZoneOptionsGeneral\").show();\n                    break;\n\n                default:\n                    $(\"#divZoneOptionsGeneralPrimaryServer\").hide();\n                    break;\n            }\n\n            //query access\n            {\n                switch (responseJSON.response.queryAccess) {\n                    case \"Allow\":\n                        $(\"#rdQueryAccessAllow\").prop(\"checked\", true);\n                        break;\n\n                    case \"AllowOnlyPrivateNetworks\":\n                        $(\"#rdQueryAccessAllowOnlyPrivateNetworks\").prop(\"checked\", true);\n                        break;\n\n                    case \"AllowOnlyZoneNameServers\":\n                        $(\"#rdQueryAccessAllowOnlyZoneNameServers\").prop(\"checked\", true);\n                        break;\n\n                    case \"UseSpecifiedNetworkACL\":\n                        $(\"#rdQueryAccessUseSpecifiedNetworkACL\").prop(\"checked\", true);\n                        $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", false);\n                        break;\n\n                    case \"AllowZoneNameServersAndUseSpecifiedNetworkACL\":\n                        $(\"#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"checked\", true);\n                        $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", false);\n                        break;\n\n                    case \"Deny\":\n                    default:\n                        $(\"#rdQueryAccessDeny\").prop(\"checked\", true);\n                        break;\n                }\n\n                switch (responseJSON.response.type) {\n                    case \"Stub\":\n                    case \"Forwarder\":\n                    case \"SecondaryForwarder\":\n                    case \"Catalog\":\n                    case \"SecondaryCatalog\":\n                        $(\"#divQueryAccessAllowOnlyZoneNameServers\").hide();\n                        $(\"#divQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").hide();\n                        break;\n\n                    default:\n                        $(\"#divQueryAccessAllowOnlyZoneNameServers\").show();\n                        $(\"#divQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").show();\n                        break;\n                }\n\n                {\n                    var value = \"\";\n\n                    for (var i = 0; i < responseJSON.response.queryAccessNetworkACL.length; i++)\n                        value += responseJSON.response.queryAccessNetworkACL[i] + \"\\r\\n\";\n\n                    $(\"#txtQueryAccessNetworkACL\").val(value);\n                }\n\n                switch (responseJSON.response.type) {\n                    case \"Primary\":\n                    case \"Forwarder\":\n                    case \"Catalog\":\n                        if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogQueryAccess) {\n                            $(\"#rdQueryAccessDeny\").prop(\"disabled\", false);\n                            $(\"#rdQueryAccessAllow\").prop(\"disabled\", false);\n                            $(\"#rdQueryAccessAllowOnlyPrivateNetworks\").prop(\"disabled\", false);\n                            $(\"#rdQueryAccessAllowOnlyZoneNameServers\").prop(\"disabled\", false);\n                            $(\"#rdQueryAccessUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n                            $(\"#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n\n                            $(\"#tabListZoneOptionsQueryAccess\").show();\n                        }\n                        else {\n                            $(\"#tabListZoneOptionsQueryAccess\").hide();\n                        }\n\n                        break;\n\n                    case \"Stub\":\n                        if ((responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember) {\n                            if (responseJSON.response.overrideCatalogQueryAccess) {\n                                $(\"#rdQueryAccessDeny\").prop(\"disabled\", true);\n                                $(\"#rdQueryAccessAllow\").prop(\"disabled\", true);\n                                $(\"#rdQueryAccessAllowOnlyPrivateNetworks\").prop(\"disabled\", true);\n                                $(\"#rdQueryAccessAllowOnlyZoneNameServers\").prop(\"disabled\", true);\n                                $(\"#rdQueryAccessUseSpecifiedNetworkACL\").prop(\"disabled\", true);\n                                $(\"#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", true);\n                                $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", true);\n\n                                $(\"#tabListZoneOptionsQueryAccess\").show();\n                            }\n                            else {\n                                $(\"#tabListZoneOptionsQueryAccess\").hide();\n                            }\n                        }\n                        else {\n                            if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogQueryAccess) {\n                                $(\"#rdQueryAccessDeny\").prop(\"disabled\", false);\n                                $(\"#rdQueryAccessAllow\").prop(\"disabled\", false);\n                                $(\"#rdQueryAccessAllowOnlyPrivateNetworks\").prop(\"disabled\", false);\n                                $(\"#rdQueryAccessAllowOnlyZoneNameServers\").prop(\"disabled\", false);\n                                $(\"#rdQueryAccessUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n                                $(\"#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n\n                                $(\"#tabListZoneOptionsQueryAccess\").show();\n                            }\n                            else {\n                                $(\"#tabListZoneOptionsQueryAccess\").hide();\n                            }\n                        }\n\n                        break;\n\n                    case \"Secondary\":\n                    case \"SecondaryForwarder\":\n                        if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogQueryAccess) {\n                            $(\"#rdQueryAccessDeny\").prop(\"disabled\", responseJSON.response.catalog != null);\n                            $(\"#rdQueryAccessAllow\").prop(\"disabled\", responseJSON.response.catalog != null);\n                            $(\"#rdQueryAccessAllowOnlyPrivateNetworks\").prop(\"disabled\", responseJSON.response.catalog != null);\n                            $(\"#rdQueryAccessAllowOnlyZoneNameServers\").prop(\"disabled\", responseJSON.response.catalog != null);\n                            $(\"#rdQueryAccessUseSpecifiedNetworkACL\").prop(\"disabled\", responseJSON.response.catalog != null);\n                            $(\"#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", responseJSON.response.catalog != null);\n\n                            if (responseJSON.response.catalog != null)\n                                $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", true);\n\n                            $(\"#tabListZoneOptionsQueryAccess\").show();\n                        }\n                        else {\n                            $(\"#tabListZoneOptionsQueryAccess\").hide();\n                        }\n\n                        break;\n\n                    case \"SecondaryCatalog\":\n                        $(\"#rdQueryAccessDeny\").prop(\"disabled\", true);\n                        $(\"#rdQueryAccessAllow\").prop(\"disabled\", true);\n                        $(\"#rdQueryAccessAllowOnlyPrivateNetworks\").prop(\"disabled\", true);\n                        $(\"#rdQueryAccessAllowOnlyZoneNameServers\").prop(\"disabled\", true);\n                        $(\"#rdQueryAccessUseSpecifiedNetworkACL\").prop(\"disabled\", true);\n                        $(\"#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", true);\n                        $(\"#txtQueryAccessNetworkACL\").prop(\"disabled\", true);\n\n                        $(\"#tabListZoneOptionsQueryAccess\").show();\n                        break;\n\n                    default:\n                        $(\"#tabListZoneOptionsQueryAccess\").hide();\n                        break;\n                }\n            }\n\n            //zone transfer\n            switch (responseJSON.response.type) {\n                case \"Primary\":\n                case \"Secondary\":\n                case \"Forwarder\":\n                case \"Catalog\":\n                case \"SecondaryCatalog\":\n                    switch (responseJSON.response.zoneTransfer) {\n                        case \"Allow\":\n                            $(\"#rdZoneTransferAllow\").prop(\"checked\", true);\n                            break;\n\n                        case \"AllowOnlyZoneNameServers\":\n                            $(\"#rdZoneTransferAllowOnlyZoneNameServers\").prop(\"checked\", true);\n                            break;\n\n                        case \"UseSpecifiedNetworkACL\":\n                            $(\"#rdZoneTransferUseSpecifiedNetworkACL\").prop(\"checked\", true);\n                            $(\"#txtZoneTransferNetworkACL\").prop(\"disabled\", false);\n                            break;\n\n                        case \"AllowZoneNameServersAndUseSpecifiedNetworkACL\":\n                            $(\"#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"checked\", true);\n                            $(\"#txtZoneTransferNetworkACL\").prop(\"disabled\", false);\n                            break;\n\n                        case \"Deny\":\n                        default:\n                            $(\"#rdZoneTransferDeny\").prop(\"checked\", true);\n                            break;\n                    }\n\n                    {\n                        var value = \"\";\n\n                        for (var i = 0; i < responseJSON.response.zoneTransferNetworkACL.length; i++)\n                            value += responseJSON.response.zoneTransferNetworkACL[i] + \"\\r\\n\";\n\n                        $(\"#txtZoneTransferNetworkACL\").val(value);\n                    }\n\n                    {\n                        var value = \"\";\n\n                        if (responseJSON.response.zoneTransferTsigKeyNames != null) {\n                            for (var i = 0; i < responseJSON.response.zoneTransferTsigKeyNames.length; i++) {\n                                value += responseJSON.response.zoneTransferTsigKeyNames[i] + \"\\r\\n\";\n                            }\n                        }\n\n                        $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").val(value);\n                    }\n\n                    {\n                        var options = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n                        if (responseJSON.response.availableTsigKeyNames != null) {\n                            for (var i = 0; i < responseJSON.response.availableTsigKeyNames.length; i++) {\n                                options += \"<option>\" + htmlEncode(responseJSON.response.availableTsigKeyNames[i]) + \"</option>\";\n                            }\n                        }\n\n                        $(\"#optZoneOptionsQuickTsigKeyNames\").html(options);\n                    }\n\n                    switch (responseJSON.response.type) {\n                        case \"Forwarder\":\n                        case \"Catalog\":\n                        case \"SecondaryCatalog\":\n                            $(\"#divZoneTransferAllowOnlyZoneNameServers\").hide();\n                            $(\"#divZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\").hide();\n                            break;\n\n                        default:\n                            $(\"#divZoneTransferAllowOnlyZoneNameServers\").show();\n                            $(\"#divZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\").show();\n                            break;\n                    }\n\n                    switch (responseJSON.response.type) {\n                        case \"Primary\":\n                        case \"Forwarder\":\n                            if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogZoneTransfer) {\n                                $(\"#rdZoneTransferDeny\").prop(\"disabled\", false);\n                                $(\"#rdZoneTransferAllow\").prop(\"disabled\", false);\n                                $(\"#rdZoneTransferAllowOnlyZoneNameServers\").prop(\"disabled\", false);\n                                $(\"#rdZoneTransferUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n                                $(\"#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n                                $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").prop(\"disabled\", false);\n                                $(\"#optZoneOptionsQuickTsigKeyNames\").prop(\"disabled\", false);\n\n                                $(\"#tabListZoneOptionsZoneTranfer\").show();\n                            }\n                            else {\n                                $(\"#tabListZoneOptionsZoneTranfer\").hide();\n                            }\n\n                            break;\n\n                        case \"Secondary\":\n                            if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogZoneTransfer) {\n                                $(\"#rdZoneTransferDeny\").prop(\"disabled\", responseJSON.response.catalog != null);\n                                $(\"#rdZoneTransferAllow\").prop(\"disabled\", responseJSON.response.catalog != null);\n                                $(\"#rdZoneTransferAllowOnlyZoneNameServers\").prop(\"disabled\", responseJSON.response.catalog != null);\n                                $(\"#rdZoneTransferUseSpecifiedNetworkACL\").prop(\"disabled\", responseJSON.response.catalog != null);\n                                $(\"#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", responseJSON.response.catalog != null);\n\n                                if (responseJSON.response.catalog != null)\n                                    $(\"#txtZoneTransferNetworkACL\").prop(\"disabled\", true);\n\n                                $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").prop(\"disabled\", responseJSON.response.catalog != null);\n                                $(\"#optZoneOptionsQuickTsigKeyNames\").prop(\"disabled\", responseJSON.response.catalog != null);\n\n                                $(\"#tabListZoneOptionsZoneTranfer\").show();\n                            }\n                            else {\n                                $(\"#tabListZoneOptionsZoneTranfer\").hide();\n                            }\n\n                            break;\n\n                        case \"Catalog\":\n                            $(\"#rdZoneTransferDeny\").prop(\"disabled\", false);\n                            $(\"#rdZoneTransferAllow\").prop(\"disabled\", false);\n                            $(\"#rdZoneTransferAllowOnlyZoneNameServers\").prop(\"disabled\", false);\n                            $(\"#rdZoneTransferUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n                            $(\"#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", false);\n                            $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").prop(\"disabled\", false);\n                            $(\"#optZoneOptionsQuickTsigKeyNames\").prop(\"disabled\", false);\n\n                            $(\"#tabListZoneOptionsZoneTranfer\").show();\n                            break;\n\n                        case \"SecondaryCatalog\":\n                            $(\"#rdZoneTransferDeny\").prop(\"disabled\", true);\n                            $(\"#rdZoneTransferAllow\").prop(\"disabled\", true);\n                            $(\"#rdZoneTransferAllowOnlyZoneNameServers\").prop(\"disabled\", true);\n                            $(\"#rdZoneTransferUseSpecifiedNetworkACL\").prop(\"disabled\", true);\n                            $(\"#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"disabled\", true);\n                            $(\"#txtZoneTransferNetworkACL\").prop(\"disabled\", true);\n                            $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").prop(\"disabled\", true);\n                            $(\"#optZoneOptionsQuickTsigKeyNames\").prop(\"disabled\", true);\n\n                            $(\"#tabListZoneOptionsZoneTranfer\").show();\n                            break;\n                    }\n\n                    break;\n\n                default:\n                    $(\"#tabListZoneOptionsZoneTranfer\").hide();\n                    break;\n            }\n\n            //notify\n            switch (responseJSON.response.type) {\n                case \"Primary\":\n                case \"Secondary\":\n                case \"Forwarder\":\n                case \"Catalog\":\n                    switch (responseJSON.response.notify) {\n                        case \"ZoneNameServers\":\n                            $(\"#rdZoneNotifyZoneNameServers\").prop(\"checked\", true);\n                            break;\n\n                        case \"SpecifiedNameServers\":\n                            $(\"#rdZoneNotifySpecifiedNameServers\").prop(\"checked\", true);\n                            $(\"#txtZoneNotifyNameServers\").prop(\"disabled\", false);\n                            break;\n\n                        case \"BothZoneAndSpecifiedNameServers\":\n                            $(\"#rdZoneNotifyBothZoneAndSpecifiedNameServers\").prop(\"checked\", true);\n                            $(\"#txtZoneNotifyNameServers\").prop(\"disabled\", false);\n                            break;\n\n                        case \"SeparateNameServersForCatalogAndMemberZones\":\n                            $(\"#rdZoneNotifySeparateNameServersForCatalogAndMemberZones\").prop(\"checked\", true);\n                            $(\"#txtZoneNotifyNameServers\").prop(\"disabled\", false);\n                            $(\"#txtZoneNotifySecondaryCatalogNameServers\").prop(\"disabled\", false);\n                            break;\n\n                        case \"None\":\n                        default:\n                            $(\"#rdZoneNotifyNone\").prop(\"checked\", true);\n                            break;\n                    }\n\n                    {\n                        var value = \"\";\n\n                        for (var i = 0; i < responseJSON.response.notifyNameServers.length; i++)\n                            value += responseJSON.response.notifyNameServers[i] + \"\\r\\n\";\n\n                        $(\"#txtZoneNotifyNameServers\").val(value);\n                    }\n\n                    if (responseJSON.response.notifySecondaryCatalogsNameServers != null) {\n                        var value = \"\";\n\n                        for (var i = 0; i < responseJSON.response.notifySecondaryCatalogsNameServers.length; i++)\n                            value += responseJSON.response.notifySecondaryCatalogsNameServers[i] + \"\\r\\n\";\n\n                        $(\"#txtZoneNotifySecondaryCatalogNameServers\").val(value);\n                    }\n                    else {\n                        $(\"#txtZoneNotifySecondaryCatalogNameServers\").val(\"\");\n                    }\n\n                    if (responseJSON.response.notifyFailed) {\n                        var value = \"\";\n\n                        for (var i = 0; i < responseJSON.response.notifyFailedFor.length; i++) {\n                            if (i == 0)\n                                value = responseJSON.response.notifyFailedFor[i];\n                            else\n                                value += \", \" + responseJSON.response.notifyFailedFor[i];\n                        }\n\n                        if ((responseJSON.response.catalog != null) && !responseJSON.response.overrideCatalogNotify) {\n                            $(\"#divZoneOptionsCatalogNotifyFailedNameServers\").show();\n                            $(\"#lblZoneOptionsCatalogNotifyFailedNameServers\").text(value);\n                        }\n\n                        $(\"#divZoneNotifyFailedNameServers\").show();\n                        $(\"#lblZoneNotifyFailedNameServers\").text(value);\n                    }\n                    else {\n                        $(\"#divZoneNotifyFailedNameServers\").hide();\n                    }\n\n                    switch (responseJSON.response.type) {\n                        case \"Forwarder\":\n                            $(\"#divZoneNotifyZoneNameServers\").hide();\n                            $(\"#divZoneNotifyBothZoneAndSpecifiedNameServers\").hide();\n                            $(\"#divZoneNotifySeparateNameServersForCatalogAndMemberZones\").hide();\n                            $(\"#divZoneNotifySecondaryCatalogNameServers\").hide();\n                            break;\n\n                        case \"Catalog\":\n                            $(\"#divZoneNotifyZoneNameServers\").hide();\n                            $(\"#divZoneNotifyBothZoneAndSpecifiedNameServers\").hide();\n                            $(\"#divZoneNotifySeparateNameServersForCatalogAndMemberZones\").show();\n                            $(\"#divZoneNotifySecondaryCatalogNameServers\").show();\n                            break;\n\n                        default:\n                            $(\"#divZoneNotifyZoneNameServers\").show();\n                            $(\"#divZoneNotifyBothZoneAndSpecifiedNameServers\").show();\n                            $(\"#divZoneNotifySeparateNameServersForCatalogAndMemberZones\").hide();\n                            $(\"#divZoneNotifySecondaryCatalogNameServers\").hide();\n                            break;\n                    }\n\n                    switch (responseJSON.response.type) {\n                        case \"Primary\":\n                        case \"Forwarder\":\n                            if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogNotify)\n                                $(\"#tabListZoneOptionsNotify\").show();\n                            else\n                                $(\"#tabListZoneOptionsNotify\").hide();\n\n                            break;\n\n                        case \"Secondary\":\n                        case \"Catalog\":\n                            $(\"#tabListZoneOptionsNotify\").show();\n                            break;\n                    }\n                    break;\n\n                default:\n                    $(\"#tabListZoneOptionsNotify\").hide();\n                    break;\n            }\n\n            //dynamic update\n            switch (responseJSON.response.type) {\n                case \"Primary\":\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"Forwarder\":\n                    //dynamic update\n                    switch (responseJSON.response.update) {\n                        case \"Allow\":\n                            $(\"#rdDynamicUpdateAllow\").prop(\"checked\", true);\n                            break;\n\n                        case \"AllowOnlyZoneNameServers\":\n                            $(\"#rdDynamicUpdateAllowOnlyZoneNameServers\").prop(\"checked\", true);\n                            break;\n\n                        case \"UseSpecifiedNetworkACL\":\n                            $(\"#rdDynamicUpdateUseSpecifiedNetworkACL\").prop(\"checked\", true);\n                            $(\"#txtDynamicUpdateNetworkACL\").prop(\"disabled\", false);\n                            break;\n\n                        case \"AllowZoneNameServersAndUseSpecifiedNetworkACL\":\n                            $(\"#rdDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL\").prop(\"checked\", true);\n                            $(\"#txtDynamicUpdateNetworkACL\").prop(\"disabled\", false);\n                            break;\n\n                        case \"Deny\":\n                        default:\n                            $(\"#rdDynamicUpdateDeny\").prop(\"checked\", true);\n                            break;\n                    }\n\n                    {\n                        var value = \"\";\n\n                        for (var i = 0; i < responseJSON.response.updateNetworkACL.length; i++)\n                            value += responseJSON.response.updateNetworkACL[i] + \"\\r\\n\";\n\n                        $(\"#txtDynamicUpdateNetworkACL\").val(value);\n                    }\n\n                    $(\"#tbodyDynamicUpdateSecurityPolicy\").html(\"\");\n\n                    switch (responseJSON.response.type) {\n                        case \"Primary\":\n                        case \"Forwarder\":\n                            zoneOptionsAvailableTsigKeyNames = responseJSON.response.availableTsigKeyNames;\n\n                            if (responseJSON.response.updateSecurityPolicies != null) {\n                                for (var i = 0; i < responseJSON.response.updateSecurityPolicies.length; i++)\n                                    addZoneOptionsDynamicUpdatesSecurityPolicyRow(i, responseJSON.response.updateSecurityPolicies[i].tsigKeyName, responseJSON.response.updateSecurityPolicies[i].domain, responseJSON.response.updateSecurityPolicies[i].allowedTypes);\n                            }\n\n                            $(\"#divDynamicUpdateSecurityPolicy\").show();\n                            break;\n\n                        default:\n                            $(\"#divDynamicUpdateSecurityPolicy\").hide();\n                            break;\n                    }\n\n                    switch (responseJSON.response.type) {\n                        case \"Secondary\":\n                        case \"SecondaryForwarder\":\n                        case \"Forwarder\":\n                            $(\"#divDynamicUpdateAllowOnlyZoneNameServers\").hide();\n                            $(\"#divDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL\").hide();\n                            break;\n\n                        default:\n                            $(\"#divDynamicUpdateAllowOnlyZoneNameServers\").show();\n                            $(\"#divDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL\").show();\n                            break;\n                    }\n\n                    $(\"#tabListZoneOptionsUpdate\").show();\n                    break;\n\n                default:\n                    $(\"#tabListZoneOptionsUpdate\").hide();\n                    break;\n            }\n\n            //tab focus\n            switch (responseJSON.response.type) {\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                case \"Stub\":\n                    $(\"#tabListZoneOptionsGeneral\").addClass(\"active\");\n                    $(\"#tabPaneZoneOptionsGeneral\").addClass(\"active\");\n                    $(\"#tabListZoneOptionsQueryAccess\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsQueryAccess\").removeClass(\"active\");\n                    $(\"#tabListZoneOptionsZoneTranfer\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsZoneTransfer\").removeClass(\"active\");\n                    $(\"#tabListZoneOptionsNotify\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsNotify\").removeClass(\"active\");\n                    $(\"#tabListZoneOptionsUpdate\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsUpdate\").removeClass(\"active\");\n                    break;\n\n                case \"Catalog\":\n                    $(\"#tabListZoneOptionsGeneral\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsGeneral\").removeClass(\"active\");\n                    $(\"#tabListZoneOptionsQueryAccess\").addClass(\"active\");\n                    $(\"#tabPaneZoneOptionsQueryAccess\").addClass(\"active\");\n                    $(\"#tabListZoneOptionsZoneTranfer\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsZoneTransfer\").removeClass(\"active\");\n                    $(\"#tabListZoneOptionsNotify\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsNotify\").removeClass(\"active\");\n                    $(\"#tabListZoneOptionsUpdate\").removeClass(\"active\");\n                    $(\"#tabPaneZoneOptionsUpdate\").removeClass(\"active\");\n                    break;\n\n                case \"Primary\":\n                case \"Forwarder\":\n                    if (responseJSON.response.availableCatalogZoneNames.length > 0) {\n                        $(\"#tabListZoneOptionsGeneral\").addClass(\"active\");\n                        $(\"#tabPaneZoneOptionsGeneral\").addClass(\"active\");\n                        $(\"#tabListZoneOptionsQueryAccess\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsQueryAccess\").removeClass(\"active\");\n                        $(\"#tabListZoneOptionsZoneTranfer\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsZoneTransfer\").removeClass(\"active\");\n                        $(\"#tabListZoneOptionsNotify\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsNotify\").removeClass(\"active\");\n                        $(\"#tabListZoneOptionsUpdate\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsUpdate\").removeClass(\"active\");\n                    }\n                    else {\n                        $(\"#tabListZoneOptionsGeneral\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsGeneral\").removeClass(\"active\");\n                        $(\"#tabListZoneOptionsQueryAccess\").addClass(\"active\");\n                        $(\"#tabPaneZoneOptionsQueryAccess\").addClass(\"active\");\n                        $(\"#tabListZoneOptionsZoneTranfer\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsZoneTransfer\").removeClass(\"active\");\n                        $(\"#tabListZoneOptionsNotify\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsNotify\").removeClass(\"active\");\n                        $(\"#tabListZoneOptionsUpdate\").removeClass(\"active\");\n                        $(\"#tabPaneZoneOptionsUpdate\").removeClass(\"active\");\n                    }\n                    break;\n            }\n\n            divZoneOptionsLoader.hide();\n            divZoneOptions.show();\n        },\n        error: function () {\n            divZoneOptionsLoader.hide();\n        },\n        invalidToken: function () {\n            $(\"#modalZoneOptions\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divZoneOptionsAlert,\n        objLoaderPlaceholder: divZoneOptionsLoader\n    });\n}\n\nfunction saveZoneOptions() {\n    var divZoneOptionsAlert = $(\"#divZoneOptionsAlert\");\n    var divZoneOptionsLoader = $(\"#divZoneOptionsLoader\");\n    var zone = $(\"#lblZoneOptionsZoneName\").attr(\"data-zone\");\n    var zoneType = $(\"#lblZoneOptionsZoneName\").attr(\"data-zone-type\");\n\n    //general catalog zone name\n    var catalog = $(\"#optZoneOptionsCatalogZoneName\").val();\n    if (catalog == null)\n        catalog = \"\";\n\n    var overrideCatalogQueryAccess = $(\"#chkZoneOptionsCatalogOverrideQueryAccess\").prop(\"checked\");\n    var overrideCatalogZoneTransfer = $(\"#chkZoneOptionsCatalogOverrideZoneTransfer\").prop(\"checked\");\n    var overrideCatalogNotify = $(\"#chkZoneOptionsCatalogOverrideNotify\").prop(\"checked\");\n\n    //general primary name server for secondary & stub\n    var primaryNameServerAddresses = cleanTextList($(\"#txtZoneOptionsPrimaryNameServerAddresses\").val());\n\n    switch (zoneType) {\n        case \"SecondaryForwarder\":\n        case \"SecondaryCatalog\":\n            if ((primaryNameServerAddresses.length === 0) || (primaryNameServerAddresses === \",\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter at least one primary name server address to proceed.\", divZoneOptionsAlert);\n                $(\"#txtZoneOptionsPrimaryNameServerAddresses\").trigger(\"focus\");\n                return;\n            }\n\n            break;\n    }\n\n    var primaryZoneTransferProtocol = $(\"input[name=rdPrimaryZoneTransferProtocol]:checked\").val();\n    var primaryZoneTransferTsigKeyName = $(\"#optZoneOptionsPrimaryZoneTransferTsigKeyName\").val();\n    var validateZone = $(\"#chkZoneOptionsValidateZone\").prop(\"checked\");\n\n    //query access\n    var queryAccess = $(\"input[name=rdQueryAccess]:checked\").val();\n\n    var queryAccessNetworkACL = cleanTextList($(\"#txtQueryAccessNetworkACL\").val());\n\n    //zone transfer\n    var zoneTransfer = $(\"input[name=rdZoneTransfer]:checked\").val();\n\n    var zoneTransferNetworkACL = cleanTextList($(\"#txtZoneTransferNetworkACL\").val());\n\n    if ((zoneTransferNetworkACL.length === 0) || (zoneTransferNetworkACL === \",\"))\n        zoneTransferNetworkACL = false;\n    else\n        $(\"#txtZoneTransferNetworkACL\").val(zoneTransferNetworkACL.replace(/,/g, \"\\n\"));\n\n    var zoneTransferTsigKeyNames = cleanTextList($(\"#txtZoneOptionsZoneTransferTsigKeyNames\").val());\n\n    if ((zoneTransferTsigKeyNames.length === 0) || (zoneTransferTsigKeyNames === \",\"))\n        zoneTransferTsigKeyNames = false;\n    else\n        $(\"#txtZoneOptionsZoneTransferTsigKeyNames\").val(zoneTransferTsigKeyNames.replace(/,/g, \"\\n\"));\n\n    //notify\n    var notify = $(\"input[name=rdZoneNotify]:checked\").val();\n\n    var notifyNameServers = cleanTextList($(\"#txtZoneNotifyNameServers\").val());\n\n    if ((notifyNameServers.length === 0) || (notifyNameServers === \",\"))\n        notifyNameServers = false;\n    else\n        $(\"#txtZoneNotifyNameServers\").val(notifyNameServers.replace(/,/g, \"\\n\"));\n\n    var notifySecondaryCatalogsNameServers = cleanTextList($(\"#txtZoneNotifySecondaryCatalogNameServers\").val());\n\n    if ((notifySecondaryCatalogsNameServers.length === 0) || (notifySecondaryCatalogsNameServers === \",\"))\n        notifySecondaryCatalogsNameServers = false;\n    else\n        $(\"#txtZoneNotifySecondaryCatalogNameServers\").val(notifySecondaryCatalogsNameServers.replace(/,/g, \"\\n\"));\n\n    //dynamic update\n    var update = $(\"input[name=rdDynamicUpdate]:checked\").val();\n\n    var updateNetworkACL = cleanTextList($(\"#txtDynamicUpdateNetworkACL\").val());\n\n    if ((updateNetworkACL.length === 0) || (updateNetworkACL === \",\"))\n        updateNetworkACL = false;\n    else\n        $(\"#txtDynamicUpdateNetworkACL\").val(updateNetworkACL.replace(/,/g, \"\\n\"));\n\n    var updateSecurityPolicies = serializeTableData($(\"#tableDynamicUpdateSecurityPolicy\"), 3, divZoneOptionsAlert);\n    if (updateSecurityPolicies === false)\n        return;\n\n    if (updateSecurityPolicies.length === 0)\n        updateSecurityPolicies = false;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnSaveZoneOptions\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/options/set?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone)\n            + \"&catalog=\" + encodeURIComponent(catalog) + \"&overrideCatalogQueryAccess=\" + overrideCatalogQueryAccess + \"&overrideCatalogZoneTransfer=\" + overrideCatalogZoneTransfer + \"&overrideCatalogNotify=\" + overrideCatalogNotify\n            + \"&primaryNameServerAddresses=\" + encodeURIComponent(primaryNameServerAddresses) + \"&primaryZoneTransferProtocol=\" + primaryZoneTransferProtocol + \"&primaryZoneTransferTsigKeyName=\" + encodeURIComponent(primaryZoneTransferTsigKeyName) + \"&validateZone=\" + validateZone\n            + \"&queryAccess=\" + queryAccess + \"&queryAccessNetworkACL=\" + encodeURIComponent(queryAccessNetworkACL)\n            + \"&zoneTransfer=\" + zoneTransfer + \"&zoneTransferNetworkACL=\" + encodeURIComponent(zoneTransferNetworkACL) + \"&zoneTransferTsigKeyNames=\" + encodeURIComponent(zoneTransferTsigKeyNames)\n            + \"&notify=\" + notify + \"&notifyNameServers=\" + encodeURIComponent(notifyNameServers) + \"&notifySecondaryCatalogsNameServers=\" + encodeURIComponent(notifySecondaryCatalogsNameServers)\n            + \"&update=\" + update + \"&updateNetworkACL=\" + encodeURIComponent(updateNetworkACL) + \"&updateSecurityPolicies=\" + encodeURIComponent(updateSecurityPolicies)\n            + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalZoneOptions\").modal(\"hide\");\n\n            var zonesRowId = $(\"#btnSaveZoneOptions\").attr(\"data-zones-row-id\");\n            if (zonesRowId == null) {\n                switch (zoneType) {\n                    case \"Catalog\":\n                    case \"SecondaryCatalog\":\n                        break;\n\n                    default:\n                        if ((catalog == null) || (catalog == \"\")) {\n                            $(\"#titleEditZoneCatalog\").hide();\n                            $(\"#titleEditZoneCatalog\").text(\"\");\n                        }\n                        else {\n                            $(\"#titleEditZoneCatalog\").attr(\"class\", \"label label-default\");\n                            $(\"#titleEditZoneCatalog\").text(catalog);\n                            $(\"#titleEditZoneCatalog\").show();\n                        }\n\n                        break;\n                }\n            }\n            else {\n                switch (zoneType) {\n                    case \"Catalog\":\n                    case \"SecondaryCatalog\":\n                        break;\n\n                    default:\n                        if ((catalog == null) || (catalog == \"\")) {\n                            $(\"#tagZoneCatalogName\" + zonesRowId).hide();\n                        }\n                        else {\n                            $(\"#tagZoneCatalogName\" + zonesRowId).text(catalog);\n                            $(\"#tagZoneCatalogName\" + zonesRowId).show();\n                        }\n                        break;\n                }\n            }\n\n            showAlert(\"success\", \"Options Saved!\", \"Zone options were saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n            divZoneOptionsLoader.hide();\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalZoneOptions\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divZoneOptionsAlert,\n        objLoaderPlaceholder: divZoneOptionsLoader\n    });\n}\n\nfunction showZonePermissionsModal(zone) {\n    var divEditPermissionsAlert = $(\"#divEditPermissionsAlert\");\n    var divEditPermissionsLoader = $(\"#divEditPermissionsLoader\");\n    var divEditPermissionsViewer = $(\"#divEditPermissionsViewer\");\n\n    $(\"#lblEditPermissionsName\").text(\"Zones / \" + (zone === \".\" ? \"<root>\" : zone));\n    $(\"#tbodyEditPermissionsUser\").html(\"\");\n    $(\"#tbodyEditPermissionsGroup\").html(\"\");\n\n    divEditPermissionsLoader.show();\n    divEditPermissionsViewer.hide();\n\n    var btnEditPermissionsSave = $(\"#btnEditPermissionsSave\");\n    btnEditPermissionsSave.attr(\"onclick\", \"saveZonePermissions(this); return false;\");\n    btnEditPermissionsSave.show();\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var modalEditPermissions = $(\"#modalEditPermissions\");\n    modalEditPermissions.modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/zones/permissions/get?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&includeUsersAndGroups=true\" + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#lblEditPermissionsName\").text(responseJSON.response.section + \" / \" + (responseJSON.response.subItem == \".\" ? \"<root>\" : responseJSON.response.subItem));\n\n            //user permissions\n            for (var i = 0; i < responseJSON.response.userPermissions.length; i++) {\n                addEditPermissionUserRow(i, responseJSON.response.userPermissions[i].username, responseJSON.response.userPermissions[i].canView, responseJSON.response.userPermissions[i].canModify, responseJSON.response.userPermissions[i].canDelete);\n            }\n\n            //load users list\n            var userListHtml = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n            for (var i = 0; i < responseJSON.response.users.length; i++) {\n                userListHtml += \"<option>\" + htmlEncode(responseJSON.response.users[i]) + \"</option>\";\n            }\n\n            $(\"#optEditPermissionsUserList\").html(userListHtml);\n\n            //group permissions\n            for (var i = 0; i < responseJSON.response.groupPermissions.length; i++) {\n                addEditPermissionGroupRow(i, responseJSON.response.groupPermissions[i].name, responseJSON.response.groupPermissions[i].canView, responseJSON.response.groupPermissions[i].canModify, responseJSON.response.groupPermissions[i].canDelete);\n            }\n\n            //load groups list\n            var groupListHtml = \"<option value=\\\"blank\\\" selected></option><option value=\\\"none\\\">None</option>\";\n\n            for (var i = 0; i < responseJSON.response.groups.length; i++) {\n                groupListHtml += \"<option>\" + htmlEncode(responseJSON.response.groups[i]) + \"</option>\";\n            }\n\n            $(\"#optEditPermissionsGroupList\").html(groupListHtml);\n\n            btnEditPermissionsSave.attr(\"data-zone\", responseJSON.response.subItem);\n\n            divEditPermissionsLoader.hide();\n            divEditPermissionsViewer.show();\n        },\n        error: function () {\n            divEditPermissionsLoader.hide();\n        },\n        invalidToken: function () {\n            modalEditPermissions.modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divEditPermissionsAlert,\n        objLoaderPlaceholder: divEditPermissionsLoader\n    });\n}\n\nfunction saveZonePermissions(objBtn) {\n    var btn = $(objBtn);\n    var divEditPermissionsAlert = $(\"#divEditPermissionsAlert\");\n\n    var zone = btn.attr(\"data-zone\");\n\n    var userPermissions = serializeTableData($(\"#tableEditPermissionsUser\"), 4);\n    var groupPermissions = serializeTableData($(\"#tableEditPermissionsGroup\"), 4);\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var apiUrl = \"api/zones/permissions/set?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&userPermissions=\" + encodeURIComponent(userPermissions) + \"&groupPermissions=\" + encodeURIComponent(groupPermissions);\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalEditPermissions\").modal(\"hide\");\n\n            showAlert(\"success\", \"Permissions Saved!\", \"Zone permissions were saved successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalEditPermissions\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divEditPermissionsAlert\n    });\n}\n\nfunction resyncZoneMenu(objMenuItem) {\n    var mnuItem = $(objMenuItem);\n\n    var id = mnuItem.attr(\"data-id\");\n    var zone = mnuItem.attr(\"data-zone\");\n    var zoneType = mnuItem.attr(\"data-zone-type\");\n\n    if (zoneType == \"Secondary\") {\n        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?\"))\n            return;\n    }\n    else {\n        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?\"))\n            return;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnZoneRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/zones/resync?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n\n            showAlert(\"success\", \"Resync Triggered!\", \"Zone '\" + zone + \"' resync was triggered successfully. Please check the Logs for confirmation.\");\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction resyncZone(objBtn, zone) {\n    if ($(\"#titleEditZoneType\").text() == \"Secondary\") {\n        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?\"))\n            return;\n    }\n    else {\n        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?\"))\n            return;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(objBtn);\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/resync?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Resync Triggered!\", \"Zone '\" + zone + \"' resync was triggered successfully. Please check the Logs for confirmation.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            showPageLogin();\n        }\n    });\n}\n\nfunction showAddZoneModal() {\n    $(\"#divAddZoneAlert\").html(\"\");\n\n    $(\"#txtAddZone\").val(\"\");\n    $(\"#txtAddZone\").prop(\"disabled\", false);\n    $(\"#rdAddZoneTypePrimary\").prop(\"checked\", true);\n    $(\"#chkAddZoneInitializeForwarder\").prop(\"checked\", true);\n    $(\"#fileAddZoneImportZone\").val(\"\");\n    $(\"#chkAddZoneUseSoaSerialDateScheme\").prop(\"checked\", $(\"#chkUseSoaSerialDateScheme\").prop(\"checked\"));\n    $(\"#txtAddZonePrimaryNameServerAddresses\").val(\"\");\n    $(\"#rdAddZoneZoneTransferProtocolTcp\").prop(\"checked\", true);\n    $(\"#optAddZoneTsigKeyName\").val(\"\");\n    $(\"#chkAddZoneValidateZone\").prop(\"checked\", false);\n    $(\"input[name=rdAddZoneForwarderProtocol]:radio\").attr(\"disabled\", false);\n    $(\"#rdAddZoneForwarderProtocolUdp\").prop(\"checked\", true);\n    $(\"#chkAddZoneForwarderThisServer\").prop(\"checked\", false);\n    $(\"#txtAddZoneForwarder\").prop(\"disabled\", false);\n    $(\"#txtAddZoneForwarder\").attr(\"placeholder\", \"8.8.8.8 or [2620:fe::10]\")\n    $(\"#txtAddZoneForwarder\").val(\"\");\n    $(\"#chkAddZoneForwarderDnssecValidation\").prop(\"checked\", $(\"#chkDnssecValidation\").prop(\"checked\"));\n    $(\"#rdAddZoneForwarderProxyTypeDefaultProxy\").prop(\"checked\", true);\n    $(\"#txtAddZoneForwarderProxyAddress\").prop(\"disabled\", true);\n    $(\"#txtAddZoneForwarderProxyPort\").prop(\"disabled\", true);\n    $(\"#txtAddZoneForwarderProxyUsername\").prop(\"disabled\", true);\n    $(\"#txtAddZoneForwarderProxyPassword\").prop(\"disabled\", true);\n    $(\"#txtAddZoneForwarderProxyAddress\").val(\"\");\n    $(\"#txtAddZoneForwarderProxyPort\").val(\"\");\n    $(\"#txtAddZoneForwarderProxyUsername\").val(\"\");\n    $(\"#txtAddZoneForwarderProxyPassword\").val(\"\");\n\n    $(\"#divAddZoneCatalogZone\").hide();\n    $(\"#divAddZoneInitializeForwarder\").hide();\n    $(\"#divAddZoneImportZoneFile\").show();\n    $(\"#divAddZoneUseSoaSerialDateScheme\").show();\n    $(\"#divAddZonePrimaryNameServerAddresses\").hide();\n    $(\"#divAddZoneZoneTransferProtocol\").hide();\n    $(\"#divAddZoneTsigKeyName\").hide();\n    $(\"#divAddZoneValidateZone\").hide();\n    $(\"#divAddZoneForwarderProtocol\").hide();\n    $(\"#divAddZoneForwarder\").hide();\n    $(\"#divAddZoneForwarderDnssecValidation\").hide();\n    $(\"#divAddZoneForwarderProxy\").hide();\n\n    $(\"#btnAddZone\").button('reset');\n\n    $(\"#modalAddZone\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtAddZone\").trigger(\"focus\");\n    }, 1000);\n\n    var currentValue = null;\n\n    if (sessionData.info.clusterInitialized)\n        currentValue = \"cluster-catalog.\" + sessionData.info.clusterDomain;\n\n    loadCatalogZoneNames($(\"#optAddZoneCatalogZoneName\"), currentValue, $(\"#divAddZoneAlert\"), $(\"#divAddZoneCatalogZone\"));\n}\n\nfunction loadCatalogZoneNames(jqDropDown, currentValue, divAlertPlaceholder, divCatalogZone) {\n    jqDropDown.prop(\"disabled\", true);\n    jqDropDown.attr(\"hasItems\", false);\n\n    if (currentValue == null)\n        currentValue = \"\";\n\n    if (currentValue.length == 0) {\n        jqDropDown.html(\"<option selected></option>\");\n    }\n    else {\n        jqDropDown.html(\"<option></option><option selected>\" + htmlEncode(currentValue) + \"</option>\");\n        jqDropDown.val(currentValue);\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    HTTPRequest({\n        url: \"api/zones/catalogs/list?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            loadCatalogZoneNamesFrom(responseJSON.response.catalogZoneNames, jqDropDown, currentValue);\n\n            if ((divCatalogZone != null) && (responseJSON.response.catalogZoneNames.length > 0))\n                divCatalogZone.show();\n        },\n        error: function () {\n            jqDropDown.prop(\"disabled\", false);\n        },\n        invalidToken: function () {\n            jqDropDown.prop(\"disabled\", false);\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAlertPlaceholder\n    });\n}\n\nfunction loadCatalogZoneNamesFrom(catalogZoneNames, jqDropDown, currentValue) {\n    var optionsHtml;\n\n    if ((currentValue == null) || (currentValue.length == 0))\n        optionsHtml = \"<option selected></option>\";\n    else\n        optionsHtml = \"<option></option>\";\n\n    for (var i = 0; i < catalogZoneNames.length; i++) {\n        optionsHtml += \"<option\" + (catalogZoneNames[i] === currentValue ? \" selected\" : \"\") + \">\" + htmlEncode(catalogZoneNames[i]) + \"</option>\";\n    }\n\n    jqDropDown.html(optionsHtml);\n    jqDropDown.prop(\"disabled\", false);\n    jqDropDown.attr(\"hasItems\", catalogZoneNames.length > 0);\n}\n\nfunction loadTsigKeyNames(jqDropDown, currentValue, divAlertPlaceholder) {\n    jqDropDown.prop(\"disabled\", true);\n\n    if (currentValue == null)\n        currentValue = \"\";\n\n    if (currentValue.length == 0) {\n        jqDropDown.html(\"<option selected></option>\");\n    }\n    else {\n        jqDropDown.html(\"<option></option><option selected>\" + htmlEncode(currentValue) + \"</option>\");\n        jqDropDown.val(currentValue);\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    HTTPRequest({\n        url: \"api/settings/getTsigKeyNames?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            loadTsigKeyNamesFrom(responseJSON.response.tsigKeyNames, jqDropDown, currentValue);\n        },\n        error: function () {\n            jqDropDown.prop(\"disabled\", false);\n        },\n        invalidToken: function () {\n            jqDropDown.prop(\"disabled\", false);\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAlertPlaceholder\n    });\n}\n\nfunction loadTsigKeyNamesFrom(tsigKeyNames, jqDropDown, currentValue) {\n    var optionsHtml;\n\n    if ((currentValue == null) || (currentValue.length == 0))\n        optionsHtml = \"<option selected></option>\";\n    else\n        optionsHtml = \"<option></option>\";\n\n    for (var i = 0; i < tsigKeyNames.length; i++) {\n        optionsHtml += \"<option\" + (tsigKeyNames[i] === currentValue ? \" selected\" : \"\") + \">\" + htmlEncode(tsigKeyNames[i]) + \"</option>\";\n    }\n\n    jqDropDown.html(optionsHtml);\n    jqDropDown.prop(\"disabled\", false);\n}\n\nfunction updateAddZoneFormForwarderThisServer() {\n    var useThisServer = $(\"#chkAddZoneForwarderThisServer\").prop('checked');\n\n    if (useThisServer) {\n        $(\"input[name=rdAddZoneForwarderProtocol]:radio\").attr(\"disabled\", true);\n        $(\"#rdAddZoneForwarderProtocolUdp\").prop(\"checked\", true);\n        $(\"#txtAddZoneForwarder\").attr(\"placeholder\", \"8.8.8.8 or [2620:fe::10]\")\n\n        $(\"#txtAddZoneForwarder\").prop(\"disabled\", true);\n        $(\"#txtAddZoneForwarder\").val(\"this-server\");\n\n        $(\"#divAddZoneForwarderProxy\").hide();\n    }\n    else {\n        $(\"input[name=rdAddZoneForwarderProtocol]:radio\").attr(\"disabled\", false);\n\n        $(\"#txtAddZoneForwarder\").prop(\"disabled\", false);\n        $(\"#txtAddZoneForwarder\").val(\"\");\n\n        $(\"#divAddZoneForwarderProxy\").show();\n    }\n}\n\nfunction addZone() {\n    var divAddZoneAlert = $(\"#divAddZoneAlert\");\n    var zone = $(\"#txtAddZone\").val();\n\n    if ((zone == null) || (zone === \"\")) {\n        showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to add zone.\", divAddZoneAlert);\n        $(\"#txtAddZone\").trigger(\"focus\");\n        return;\n    }\n\n    var type = $('input[name=rdAddZoneType]:checked').val();\n\n    var parameters;\n\n    switch (type) {\n        case \"Primary\":\n            var catalog = $(\"#optAddZoneCatalogZoneName\").val();\n            var useSoaSerialDateScheme = $(\"#chkAddZoneUseSoaSerialDateScheme\").prop(\"checked\");\n\n            parameters = \"&catalog=\" + encodeURIComponent(catalog) + \"&useSoaSerialDateScheme=\" + useSoaSerialDateScheme;\n            break;\n\n        case \"Secondary\":\n            var catalog = $(\"#optAddZoneCatalogZoneName\").val();\n\n            parameters = \"&catalog=\" + encodeURIComponent(catalog) + \"&primaryNameServerAddresses=\" + encodeURIComponent(cleanTextList($(\"#txtAddZonePrimaryNameServerAddresses\").val()));\n            parameters += \"&zoneTransferProtocol=\" + $(\"input[name=rdAddZoneZoneTransferProtocol]:checked\").val();\n            parameters += \"&tsigKeyName=\" + encodeURIComponent($(\"#optAddZoneTsigKeyName\").val());\n            parameters += \"&validateZone=\" + $(\"#chkAddZoneValidateZone\").prop(\"checked\");\n            break;\n\n        case \"Stub\":\n            var catalog = $(\"#optAddZoneCatalogZoneName\").val();\n\n            parameters = \"&catalog=\" + encodeURIComponent(catalog) + \"&primaryNameServerAddresses=\" + encodeURIComponent(cleanTextList($(\"#txtAddZonePrimaryNameServerAddresses\").val()));\n            break;\n\n        case \"Forwarder\":\n            var catalog = $(\"#optAddZoneCatalogZoneName\").val();\n            var initializeForwarder = $(\"#chkAddZoneInitializeForwarder\").prop(\"checked\");\n\n            if (initializeForwarder) {\n                var protocol = $(\"input[name=rdAddZoneForwarderProtocol]:checked\").val();\n\n                var forwarder = $(\"#txtAddZoneForwarder\").val();\n                if ((forwarder == null) || (forwarder === \"\")) {\n                    showAlert(\"warning\", \"Missing!\", \"Please enter a forwarder server address to add zone.\", divAddZoneAlert);\n                    $(\"#txtAddZoneForwarder\").trigger(\"focus\");\n                    return;\n                }\n\n                var dnssecValidation = $(\"#chkAddZoneForwarderDnssecValidation\").prop(\"checked\");\n\n                parameters = \"&catalog=\" + encodeURIComponent(catalog) + \"&protocol=\" + protocol + \"&forwarder=\" + encodeURIComponent(forwarder) + \"&dnssecValidation=\" + dnssecValidation;\n\n                if (forwarder !== \"this-server\") {\n                    var proxyType = $(\"input[name=rdAddZoneForwarderProxyType]:checked\").val();\n\n                    parameters += \"&proxyType=\" + proxyType;\n\n                    switch (proxyType) {\n                        case \"Http\":\n                        case \"Socks5\":\n                            var proxyAddress = $(\"#txtAddZoneForwarderProxyAddress\").val();\n                            var proxyPort = $(\"#txtAddZoneForwarderProxyPort\").val();\n                            var proxyUsername = $(\"#txtAddZoneForwarderProxyUsername\").val();\n                            var proxyPassword = $(\"#txtAddZoneForwarderProxyPassword\").val();\n\n                            if ((proxyAddress == null) || (proxyAddress === \"\")) {\n                                showAlert(\"warning\", \"Missing!\", \"Please enter a domain name or IP address for Proxy Server Address to add zone.\", divAddZoneAlert);\n                                $(\"#txtAddZoneForwarderProxyAddress\").trigger(\"focus\");\n                                return;\n                            }\n\n                            if ((proxyPort == null) || (proxyPort === \"\")) {\n                                showAlert(\"warning\", \"Missing!\", \"Please enter a port number for Proxy Server Port to add zone.\", divAddZoneAlert);\n                                $(\"#txtAddZoneForwarderProxyPort\").trigger(\"focus\");\n                                return;\n                            }\n\n                            parameters += \"&proxyAddress=\" + encodeURIComponent(proxyAddress) + \"&proxyPort=\" + proxyPort + \"&proxyUsername=\" + encodeURIComponent(proxyUsername) + \"&proxyPassword=\" + encodeURIComponent(proxyPassword);\n                            break;\n                    }\n                }\n\n                parameters += \"&initializeForwarder=true\";\n            } else {\n                parameters = \"&initializeForwarder=false\";\n            }\n\n            break;\n\n        case \"SecondaryForwarder\":\n        case \"SecondaryCatalog\":\n            var primaryNameServerAddresses = cleanTextList($(\"#txtAddZonePrimaryNameServerAddresses\").val());\n            if ((primaryNameServerAddresses.length === 0) || (primaryNameServerAddresses === \",\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter at least one primary name server address to proceed.\", divAddZoneAlert);\n                $(\"#txtAddZonePrimaryNameServerAddresses\").trigger(\"focus\");\n                return;\n            }\n\n            parameters = \"&primaryNameServerAddresses=\" + encodeURIComponent(primaryNameServerAddresses);\n            parameters += \"&zoneTransferProtocol=\" + $(\"input[name=rdAddZoneZoneTransferProtocol]:checked\").val();\n            parameters += \"&tsigKeyName=\" + encodeURIComponent($(\"#optAddZoneTsigKeyName\").val());\n            break;\n\n        case \"SecondaryRoot\":\n            type = \"Secondary\";\n            var catalog = $(\"#optAddZoneCatalogZoneName\").val();\n\n            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]\";\n            parameters += \"&zoneTransferProtocol=Tcp\";\n            parameters += \"&validateZone=true\";\n            break;\n\n        default:\n            parameters = \"\";\n            break;\n    }\n\n    var formData;\n\n    switch (type) {\n        case \"Primary\":\n        case \"Forwarder\":\n            var fileAddZoneImportZone = $(\"#fileAddZoneImportZone\");\n\n            if (fileAddZoneImportZone[0].files.length > 0) {\n                formData = new FormData();\n                formData.append(\"fileImportZone\", fileAddZoneImportZone[0].files[0]);\n            }\n            break;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnAddZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/create?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&type=\" + type + parameters + \"&node=\" + encodeURIComponent(node),\n        method: \"POST\",\n        data: formData,\n        contentType: false,\n        processData: false,\n        success: function (responseJSON) {\n            $(\"#modalAddZone\").modal(\"hide\");\n            showEditZone(responseJSON.response.domain);\n\n            showAlert(\"success\", \"Zone Added!\", \"Zone was added successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalAddZone\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAddZoneAlert\n    });\n}\n\nfunction toggleHideDnssecRecords(hideDnssecRecords) {\n    localStorage.setItem(\"zoneHideDnssecRecords\", hideDnssecRecords);\n    showEditZone($(\"#titleEditZone\").attr(\"data-zone\"));\n}\n\nfunction showEditZone(zone, showPageNumber, zoneFilterName, zoneFilterType) {\n    if (zone == null) {\n        zone = $(\"#txtZonesEdit\").val();\n        if (zone === \"\") {\n            showAlert(\"warning\", \"Missing!\", \"Please enter a zone name to start editing.\");\n            $(\"#txtZonesEdit\").trigger(\"focus\");\n            return;\n        }\n    }\n\n    if (showPageNumber == null)\n        showPageNumber = 1;\n\n    if (zoneFilterName == null)\n        zoneFilterName = \"\";\n\n    if (zoneFilterType == null)\n        zoneFilterType = \"\";\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var divViewZonesLoader = $(\"#divViewZonesLoader\");\n    var divViewZones = $(\"#divViewZones\");\n    var divEditZone = $(\"#divEditZone\");\n\n    divViewZones.hide();\n    divEditZone.hide();\n    divViewZonesLoader.show();\n\n    HTTPRequest({\n        url: \"api/zones/records/get?token=\" + sessionData.token + \"&domain=\" + encodeURIComponent(zone) + \"&zone=\" + encodeURIComponent(zone) + \"&listZone=true\" + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            zone = responseJSON.response.zone.name;\n            if (zone === \"\")\n                zone = \".\";\n\n            var zoneType;\n            if (responseJSON.response.zone.internal)\n                zoneType = \"Internal\";\n            else\n                zoneType = responseJSON.response.zone.type;\n\n            switch (responseJSON.response.zone.dnssecStatus) {\n                case \"SignedWithNSEC\":\n                case \"SignedWithNSEC3\":\n                    $(\"#titleEditZoneDnssecStatus\").removeClass();\n\n                    if (responseJSON.response.zone.hasDnssecPrivateKeys)\n                        $(\"#titleEditZoneDnssecStatus\").addClass(\"label label-primary\");\n                    else\n                        $(\"#titleEditZoneDnssecStatus\").addClass(\"label label-default\");\n\n                    $(\"#titleEditZoneDnssecStatus\").show();\n                    break;\n\n                default:\n                    $(\"#titleEditZoneDnssecStatus\").hide();\n                    break;\n            }\n\n            var status;\n            if (responseJSON.response.zone.disabled)\n                status = \"Disabled\";\n            else if (responseJSON.response.zone.isExpired)\n                status = \"Expired\";\n            else if (responseJSON.response.zone.validationFailed)\n                status = \"Validation Failed\";\n            else if (responseJSON.response.zone.syncFailed)\n                status = \"Sync Failed\";\n            else if (responseJSON.response.zone.notifyFailed)\n                status = \"Notify Failed\";\n            else\n                status = \"Enabled\";\n\n            if (responseJSON.response.zone.catalog != null) {\n                $(\"#titleEditZoneCatalog\").attr(\"class\", \"label label-default\");\n                $(\"#titleEditZoneCatalog\").text(responseJSON.response.zone.catalog);\n                $(\"#titleEditZoneCatalog\").show();\n            }\n            else {\n                switch (zoneType) {\n                    case \"Catalog\":\n                    case \"SecondaryCatalog\":\n                        $(\"#titleEditZoneCatalog\").attr(\"class\", \"label label-info\");\n                        $(\"#titleEditZoneCatalog\").text(zone);\n                        $(\"#titleEditZoneCatalog\").show();\n                        break;\n\n                    default:\n                        $(\"#titleEditZoneCatalog\").hide();\n                        $(\"#titleEditZoneCatalog\").text(\"\");\n                        break;\n                }\n            }\n\n            var expiry = responseJSON.response.zone.expiry;\n            if (expiry == null)\n                expiry = \"&nbsp;\";\n            else\n                expiry = \"Expiry: \" + moment(expiry).local().format(\"YYYY-MM-DD HH:mm:ss\");\n\n            switch (zoneType) {\n                case \"SecondaryForwarder\":\n                    $(\"#titleEditZoneType\").html(\"Secondary Forwarder\");\n                    break;\n\n                case \"SecondaryCatalog\":\n                    $(\"#titleEditZoneType\").html(\"Secondary Catalog\");\n                    break;\n\n                default:\n                    $(\"#titleEditZoneType\").html(zoneType);\n                    break;\n            }\n\n            $(\"#titleEditZoneStatus\").html(status);\n            $(\"#titleEditZoneExpiry\").html(expiry);\n\n            if (responseJSON.response.zone.internal)\n                $(\"#titleEditZoneType\").attr(\"class\", \"label label-default\");\n            else\n                $(\"#titleEditZoneType\").attr(\"class\", \"label label-primary\");\n\n            switch (status) {\n                case \"Disabled\":\n                    $(\"#titleEditZoneStatus\").attr(\"class\", \"label label-default\");\n                    break;\n\n                case \"Sync Failed\":\n                case \"Notify Failed\":\n                    $(\"#titleEditZoneStatus\").attr(\"class\", \"label label-warning\");\n                    break;\n\n                case \"Expired\":\n                case \"Validation Failed\":\n                    $(\"#titleEditZoneStatus\").attr(\"class\", \"label label-danger\");\n                    break;\n\n                default:\n                    $(\"#titleEditZoneStatus\").attr(\"class\", \"label label-success\");\n                    break;\n            }\n\n            switch (zoneType) {\n                case \"Internal\":\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                case \"Stub\":\n                case \"Catalog\":\n                    $(\"#btnEditZoneAddRecord\").hide();\n                    break;\n\n                case \"Forwarder\":\n                    $(\"#btnEditZoneAddRecord\").show();\n                    $(\"#optAddEditRecordTypeDs\").hide();\n                    $(\"#optAddEditRecordTypeSshfp\").hide();\n                    $(\"#optAddEditRecordTypeTlsa\").hide();\n                    $(\"#optAddEditRecordTypeAName\").show();\n                    $(\"#optAddEditRecordTypeFwd\").show();\n                    $(\"#optAddEditRecordTypeApp\").show();\n                    break;\n\n                case \"Primary\":\n                    $(\"#btnEditZoneAddRecord\").show();\n                    $(\"#optAddEditRecordTypeFwd\").hide();\n\n                    switch (responseJSON.response.zone.dnssecStatus) {\n                        case \"SignedWithNSEC\":\n                        case \"SignedWithNSEC3\":\n                            $(\"#optAddEditRecordTypeDs\").show();\n                            $(\"#optAddEditRecordTypeSshfp\").show();\n                            $(\"#optAddEditRecordTypeTlsa\").show();\n                            $(\"#optAddEditRecordTypeAName\").hide();\n                            $(\"#optAddEditRecordTypeApp\").hide();\n                            break;\n\n                        default:\n                            $(\"#optAddEditRecordTypeDs\").hide();\n                            $(\"#optAddEditRecordTypeSshfp\").hide();\n                            $(\"#optAddEditRecordTypeTlsa\").hide();\n                            $(\"#optAddEditRecordTypeAName\").show();\n                            $(\"#optAddEditRecordTypeApp\").show();\n                            break;\n                    }\n                    break;\n            }\n\n            if (responseJSON.response.zone.internal) {\n                $(\"#btnEnableZoneEditZone\").hide();\n                $(\"#btnDisableZoneEditZone\").hide();\n                $(\"#btnEditZoneDeleteZone\").hide();\n            }\n            else if (responseJSON.response.zone.disabled) {\n                $(\"#btnEnableZoneEditZone\").show();\n                $(\"#btnDisableZoneEditZone\").hide();\n                $(\"#btnEditZoneDeleteZone\").show();\n            }\n            else {\n                $(\"#btnEnableZoneEditZone\").hide();\n                $(\"#btnDisableZoneEditZone\").show();\n                $(\"#btnEditZoneDeleteZone\").show();\n            }\n\n            switch (zoneType) {\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                case \"Stub\":\n                    $(\"#btnZoneResync\").show();\n                    break;\n\n                default:\n                    $(\"#btnZoneResync\").hide();\n                    break;\n            }\n\n            switch (zoneType) {\n                case \"Primary\":\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                case \"Stub\":\n                case \"Forwarder\":\n                case \"Catalog\":\n                    $(\"#divOptionsMenu\").show();\n                    break;\n\n                default:\n                    $(\"#divOptionsMenu\").hide();\n                    break;\n            }\n\n            switch (zoneType) {\n                case \"Primary\":\n                case \"Forwarder\":\n                    $(\"#lnkImportZone\").show();\n                    $(\"#lnkExportZone\").show();\n                    break;\n\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                case \"Catalog\":\n                    $(\"#lnkImportZone\").hide();\n                    $(\"#lnkExportZone\").show();\n                    break;\n\n                default:\n                    $(\"#lnkImportZone\").hide();\n                    $(\"#lnkExportZone\").hide();\n                    break;\n            }\n\n            switch (zoneType) {\n                case \"Primary\":\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"Forwarder\":\n                case \"SecondaryCatalog\":\n                    $(\"#lnkZoneConvert\").show();\n                    break;\n\n                default:\n                    $(\"#lnkZoneConvert\").hide();\n                    break;\n            }\n\n            switch (zoneType) {\n                case \"Primary\":\n                case \"Forwarder\":\n                    $(\"#lnkCloneZone\").show();\n                    break;\n\n                default:\n                    $(\"#lnkCloneZone\").hide();\n                    break;\n            }\n\n            switch (zoneType) {\n                case \"Primary\":\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                case \"Stub\":\n                case \"Forwarder\":\n                case \"Catalog\":\n                    $(\"#lnkZoneOptions\").show();\n                    break;\n\n                default:\n                    $(\"#lnkZoneOptions\").hide();\n                    break;\n            }\n\n            switch (zoneType) {\n                case \"Primary\":\n                case \"Secondary\":\n                case \"SecondaryForwarder\":\n                case \"SecondaryCatalog\":\n                case \"Stub\":\n                case \"Forwarder\":\n                case \"Catalog\":\n                    $(\"#btnZonePermissions\").show();\n                    break;\n\n                default:\n                    $(\"#btnZonePermissions\").hide();\n                    break;\n            }\n\n            var zoneHideDnssecRecords = (localStorage.getItem(\"zoneHideDnssecRecords\") == \"true\");\n\n            switch (zoneType) {\n                case \"Primary\":\n                    $(\"#divZoneDnssecOptions\").show();\n\n                    switch (responseJSON.response.zone.dnssecStatus) {\n                        case \"SignedWithNSEC\":\n                        case \"SignedWithNSEC3\":\n                            $(\"#lnkZoneDnssecSignZone\").hide();\n\n                            if (zoneHideDnssecRecords) {\n                                $(\"#lnkZoneDnssecHideRecords\").hide();\n                                $(\"#lnkZoneDnssecShowRecords\").show();\n                            }\n                            else {\n                                $(\"#lnkZoneDnssecHideRecords\").show();\n                                $(\"#lnkZoneDnssecShowRecords\").hide();\n                            }\n\n                            $(\"#lnkZoneDnssecViewDsRecords\").show();\n                            $(\"#lnkZoneDnssecProperties\").show();\n                            $(\"#lnkZoneDnssecUnsignZone\").show();\n                            break;\n\n                        default:\n                            $(\"#lnkZoneDnssecSignZone\").show();\n                            $(\"#lnkZoneDnssecHideRecords\").hide();\n                            $(\"#lnkZoneDnssecShowRecords\").hide();\n                            $(\"#lnkZoneDnssecViewDsRecords\").hide();\n                            $(\"#lnkZoneDnssecProperties\").hide();\n                            $(\"#lnkZoneDnssecUnsignZone\").hide();\n                            break;\n                    }\n                    break;\n\n                case \"Secondary\":\n                    switch (responseJSON.response.zone.dnssecStatus) {\n                        case \"SignedWithNSEC\":\n                        case \"SignedWithNSEC3\":\n                            $(\"#divZoneDnssecOptions\").show();\n\n                            $(\"#lnkZoneDnssecSignZone\").hide();\n\n                            if (zoneHideDnssecRecords) {\n                                $(\"#lnkZoneDnssecHideRecords\").hide();\n                                $(\"#lnkZoneDnssecShowRecords\").show();\n                            }\n                            else {\n                                $(\"#lnkZoneDnssecHideRecords\").show();\n                                $(\"#lnkZoneDnssecShowRecords\").hide();\n                            }\n\n                            $(\"#lnkZoneDnssecViewDsRecords\").hide();\n                            $(\"#lnkZoneDnssecProperties\").hide();\n                            $(\"#lnkZoneDnssecUnsignZone\").hide();\n                            break;\n\n                        default:\n                            $(\"#divZoneDnssecOptions\").hide();\n                            break;\n                    }\n                    break;\n\n                default:\n                    $(\"#divZoneDnssecOptions\").hide();\n                    break;\n            }\n\n            editZoneInfo = responseJSON.response.zone;\n\n            if (!zoneHideDnssecRecords || (responseJSON.response.zone.dnssecStatus === \"Unsigned\")) {\n                editZoneRecords = responseJSON.response.records;\n            }\n            else {\n                var records = responseJSON.response.records;\n                editZoneRecords = [];\n\n                for (var i = 0; i < records.length; i++) {\n                    switch (records[i].type.toUpperCase()) {\n                        case \"RRSIG\":\n                        case \"NSEC\":\n                        case \"DNSKEY\":\n                        case \"NSEC3\":\n                        case \"NSEC3PARAM\":\n                            continue;\n\n                        default:\n                            editZoneRecords.push(records[i]);\n                            break;\n                    }\n                }\n            }\n\n            $(\"#optEditZoneClusterNode\").val(node);\n\n            if (responseJSON.response.zone.nameIdn == null)\n                $(\"#titleEditZone\").text(zone === \".\" ? \"<root>\" : zone);\n            else\n                $(\"#titleEditZone\").text(responseJSON.response.zone.nameIdn + \" (\" + zone + \")\");\n\n            $(\"#titleEditZone\").attr(\"data-zone\", zone);\n            $(\"#titleEditZone\").attr(\"data-zone-type\", zoneType);\n\n            $(\"#txtEditZoneFilterName\").val(zoneFilterName);\n            $(\"#txtEditZoneFilterType\").val(zoneFilterType);\n            editZoneFilteredRecords = null; //to evaluate filters again\n\n            showEditZonePage(showPageNumber);\n\n            divViewZonesLoader.hide();\n            divEditZone.show();\n        },\n        error: function () {\n            divViewZonesLoader.hide();\n            divViewZones.show();\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objLoaderPlaceholder: divViewZonesLoader\n    });\n}\n\nfunction showEditZonePage(pageNumber) {\n    var filterName = $(\"#txtEditZoneFilterName\").val();\n    if (filterName === \"\")\n        filterName = null;\n\n    var filterType = $(\"#txtEditZoneFilterType\").val();\n    if (filterType === \"\")\n        filterType = null;\n\n    if (pageNumber == null)\n        pageNumber = Number($(\"#txtEditZonePageNumber\").val());\n\n    if (pageNumber == 0)\n        pageNumber = 1;\n\n    var recordsPerPage = Number($(\"#optEditZoneRecordsPerPage\").val());\n    if (recordsPerPage < 1)\n        recordsPerPage = 10;\n\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n    var zoneType = $(\"#titleEditZone\").attr(\"data-zone-type\");\n\n    if (editZoneFilteredRecords == null) {\n        if ((filterName != null) || (filterType != null)) {\n            editZoneFilteredRecords = [];\n            var filterDomain = null;\n            var filterRegex = null;\n\n            if (filterName != null) {\n                filterDomain = filterName.toLowerCase();\n\n                if (zone == \".\") {\n                    if (filterDomain === \"@\")\n                        filterDomain = \"\";\n                }\n                else {\n                    if (filterDomain === \"@\")\n                        filterDomain = zone;\n                    else\n                        filterDomain += \".\" + zone;\n                }\n\n                if ((filterName.indexOf(\"*\") > -1) || (filterName.indexOf(\"?\") > -1)) {\n                    filterDomain = filterDomain.replace(/\\./g, \"\\\\\\.\");\n                    filterDomain = filterDomain.replace(/\\*/g, \".*\");\n                    filterDomain = filterDomain.replace(/\\?/g, \".\");\n\n                    if (filterDomain.startsWith(\".*\\\\\\.\"))\n                        filterDomain = \"\\\\\\*\" + filterDomain.substring(2);\n\n                    filterRegex = new RegExp(\"^\" + filterDomain + \"$\");\n                }\n            }\n\n            if (filterType != null)\n                filterType = filterType.toUpperCase();\n\n            for (var i = 0; i < editZoneRecords.length; i++) {\n                if (filterRegex == null) {\n                    if ((filterDomain != null) && (editZoneRecords[i].name.toLowerCase() !== filterDomain))\n                        continue;\n                }\n                else if (!filterRegex.test(editZoneRecords[i].name.toLowerCase())) {\n                    continue;\n                }\n\n                if ((filterType != null) && (editZoneRecords[i].type !== filterType))\n                    continue;\n\n                editZoneRecords[i].index = i; //keep original index for update tasks\n\n                editZoneFilteredRecords.push(editZoneRecords[i]);\n            }\n        }\n        else {\n            for (var i = 0; i < editZoneRecords.length; i++)\n                editZoneRecords[i].index = i; //keep original index for update tasks\n\n            editZoneFilteredRecords = editZoneRecords;\n        }\n    }\n\n    var totalRecords = editZoneFilteredRecords.length;\n    var totalPages = Math.floor(totalRecords / recordsPerPage) + (totalRecords % recordsPerPage > 0 ? 1 : 0);\n\n    if ((pageNumber > totalPages) || (pageNumber < 0))\n        pageNumber = totalPages;\n\n    if (pageNumber < 1)\n        pageNumber = 1;\n\n    var start = (pageNumber - 1) * recordsPerPage;\n    var end = Math.min(start + recordsPerPage, totalRecords);\n\n    var tableHtmlRows = \"\";\n\n    for (var i = start; i < end; i++)\n        tableHtmlRows += getZoneRecordRowHtml(i, zone, zoneType, editZoneFilteredRecords[i]);\n\n    var paginationHtml = \"\";\n\n    if (pageNumber > 1) {\n        paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"First\\\" onClick=\\\"showEditZonePage(1); return false;\\\"><span aria-hidden=\\\"true\\\">&laquo;</span></a></li>\";\n        paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Previous\\\" onClick=\\\"showEditZonePage(\" + (pageNumber - 1) + \"); return false;\\\"><span aria-hidden=\\\"true\\\">&lsaquo;</span></a></li>\";\n    }\n\n    var pageStart = pageNumber - 5;\n    if (pageStart < 1)\n        pageStart = 1;\n\n    var pageEnd = pageStart + 9;\n    if (pageEnd > totalPages) {\n        var endDiff = pageEnd - totalPages;\n        pageEnd = totalPages;\n\n        pageStart -= endDiff;\n        if (pageStart < 1)\n            pageStart = 1;\n    }\n\n    for (var i = pageStart; i <= pageEnd; i++) {\n        if (i == pageNumber)\n            paginationHtml += \"<li class=\\\"active\\\"><a href=\\\"#\\\" onClick=\\\"showEditZonePage(\" + i + \"); return false;\\\">\" + i + \"</a></li>\";\n        else\n            paginationHtml += \"<li><a href=\\\"#\\\" onClick=\\\"showEditZonePage(\" + i + \"); return false;\\\">\" + i + \"</a></li>\";\n    }\n\n    if (pageNumber < totalPages) {\n        paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Next\\\" onClick=\\\"showEditZonePage(\" + (pageNumber + 1) + \"); return false;\\\"><span aria-hidden=\\\"true\\\">&rsaquo;</span></a></li>\";\n        paginationHtml += \"<li><a href=\\\"#\\\" aria-label=\\\"Last\\\" onClick=\\\"showEditZonePage(-1); return false;\\\"><span aria-hidden=\\\"true\\\">&raquo;</span></a></li>\";\n    }\n\n    var statusHtml;\n\n    if (editZoneFilteredRecords.length > 0)\n        statusHtml = (start + 1) + \"-\" + end + \" (\" + (end - start) + \") of \" + editZoneFilteredRecords.length + \" records (page \" + pageNumber + \" of \" + totalPages + \")\";\n    else\n        statusHtml = \"0 records\";\n\n    $(\"#txtEditZonePageNumber\").val(pageNumber);\n    $(\"#tableEditZoneBody\").html(tableHtmlRows);\n\n    $(\"#tableEditZoneTopStatus\").html(statusHtml);\n    $(\"#tableEditZoneTopPagination\").html(paginationHtml);\n\n    $(\"#tableEditZoneFooterStatus\").html(statusHtml);\n    $(\"#tableEditZoneFooterPagination\").html(paginationHtml);\n}\n\nfunction getZoneRecordRowHtml(index, zone, zoneType, record) {\n    var name = record.name;\n    if (name === \"\")\n        name = \".\";\n\n    var lowerName = name.toLowerCase();\n\n    if (lowerName === zone) {\n        name = \"@\";\n    } else {\n        var i = lowerName.lastIndexOf(\".\" + zone)\n        if (i > -1)\n            name = name.substring(0, i);\n    }\n\n    var tableHtmlRow = \"<tr id=\\\"trZoneRecord\" + index + \"\\\"><td>\" + (index + 1) + \"</td><td>\" + htmlEncode(name) + \"</td>\";\n    tableHtmlRow += \"<td>\" + record.type + \"</td>\";\n    tableHtmlRow += \"<td>\" + record.ttl + \"<br />(\" + record.ttlString + \")</td>\";\n\n    var additionalDataAttributes = \"\";\n\n    tableHtmlRow += \"<td style=\\\"word-break: break-all;\\\">\";\n\n    switch (record.type.toUpperCase()) {\n        case \"A\":\n        case \"AAAA\":\n            tableHtmlRow += htmlEncode(record.rData.ipAddress);\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-ip-address=\\\"\" + htmlEncode(record.rData.ipAddress) + \"\\\" \";\n            break;\n\n        case \"NS\":\n            var notifyFailed = false;\n\n            if (editZoneInfo.notifyFailedFor != null) {\n                for (var i = 0; i < editZoneInfo.notifyFailedFor.length; i++) {\n                    if (editZoneInfo.notifyFailedFor[i] == record.rData.nameServer) {\n                        notifyFailed = true;\n                        break;\n                    }\n                }\n            }\n\n            tableHtmlRow += \"<b>Name Server:</b> \" + htmlEncode(record.rData.nameServer);\n\n            if (notifyFailed)\n                tableHtmlRow += \"<span class=\\\"label label-warning\\\" style=\\\"margin-left: 8px;\\\">Notify Failed</span>\";\n\n            if (record.glueRecords != null) {\n                var glue = null;\n\n                for (var i = 0; i < record.glueRecords.length; i++) {\n                    if (i == 0)\n                        glue = record.glueRecords[i];\n                    else\n                        glue += \", \" + record.glueRecords[i];\n                }\n\n                tableHtmlRow += \"<br /><b>Glue Addresses:</b> \" + glue;\n\n                additionalDataAttributes = \"data-record-glue=\\\"\" + htmlEncode(glue) + \"\\\" \";\n            } else {\n                additionalDataAttributes = \"data-record-glue=\\\"\\\" \";\n            }\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes += \"data-record-name-server=\\\"\" + htmlEncode(record.rData.nameServer) + \"\\\" \";\n            break;\n\n        case \"CNAME\":\n            tableHtmlRow += htmlEncode(record.rData.cname);\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-cname=\\\"\" + htmlEncode(record.rData.cname) + \"\\\" \";\n            break;\n\n        case \"SOA\":\n            tableHtmlRow += \"<b>Primary Name Server:</b> \" + htmlEncode(record.rData.primaryNameServer) +\n                \"<br /><b>Responsible Person:</b> \" + htmlEncode(record.rData.responsiblePerson) +\n                \"<br /><b>Serial:</b> \" + htmlEncode(record.rData.serial) +\n                \"<br /><b>Refresh:</b> \" + htmlEncode(record.rData.refresh + \" (\" + record.rData.refreshString + \")\") +\n                \"<br /><b>Retry:</b> \" + htmlEncode(record.rData.retry + \" (\" + record.rData.retryString + \")\") +\n                \"<br /><b>Expire:</b> \" + htmlEncode(record.rData.expire + \" (\" + record.rData.expireString + \")\") +\n                \"<br /><b>Minimum:</b> \" + htmlEncode(record.rData.minimum + \" (\" + record.rData.minimumString + \")\");\n\n            if (record.rData.useSerialDateScheme != null) {\n                tableHtmlRow += \"<br /><br /><b>Use Serial Date Scheme:</b> \" + record.rData.useSerialDateScheme;\n\n                additionalDataAttributes = \"data-record-serial-scheme=\\\"\" + htmlEncode(record.rData.useSerialDateScheme) + \"\\\" \";\n            }\n            else {\n                additionalDataAttributes = \"data-record-serial-scheme=\\\"false\\\" \";\n            }\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes += \"data-record-pname=\\\"\" + htmlEncode(record.rData.primaryNameServer) + \"\\\" \" +\n                \"data-record-rperson=\\\"\" + htmlEncode(record.rData.responsiblePerson) + \"\\\" \" +\n                \"data-record-serial=\\\"\" + htmlEncode(record.rData.serial) + \"\\\" \" +\n                \"data-record-refresh=\\\"\" + htmlEncode(record.rData.refresh) + \"\\\" \" +\n                \"data-record-retry=\\\"\" + htmlEncode(record.rData.retry) + \"\\\" \" +\n                \"data-record-expire=\\\"\" + htmlEncode(record.rData.expire) + \"\\\" \" +\n                \"data-record-minimum=\\\"\" + htmlEncode(record.rData.minimum) + \"\\\" \";\n            break;\n\n        case \"PTR\":\n            tableHtmlRow += htmlEncode(record.rData.ptrName);\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-ptr-name=\\\"\" + htmlEncode(record.rData.ptrName) + \"\\\" \";\n            break;\n\n        case \"MX\":\n            tableHtmlRow += \"<b>Preference: </b> \" + htmlEncode(record.rData.preference) +\n                \"<br /><b>Exchange:</b> \" + htmlEncode(record.rData.exchange);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-preference=\\\"\" + htmlEncode(record.rData.preference) + \"\\\" \" +\n                \"data-record-exchange=\\\"\" + htmlEncode(record.rData.exchange) + \"\\\" \";\n            break;\n\n        case \"TXT\":\n            var text;\n\n            if (record.rData.splitText) {\n                for (var i = 0; i < record.rData.characterStrings.length; i++) {\n                    var characterString = record.rData.characterStrings[i].replace(/\\\\/g, \"\\\\\\\\\").replace(/\\r/g, \"\\\\r\").replace(/\\n/g, \"\\\\n\");\n\n                    tableHtmlRow += \"\\\"\" + htmlEncode(characterString.replace(/\"/g, \"\\\\\\\"\")) + \"\\\"<br />\";\n\n                    if (text == null)\n                        text = characterString;\n                    else\n                        text += \"\\n\" + characterString;\n                }\n            }\n            else {\n                var characterString = record.rData.text.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\r/g, \"\\\\r\").replace(/\\n/g, \"\\\\n\");\n                tableHtmlRow += htmlEncode(characterString.replace(/\"/g, \"\\\\\\\"\")) + \"<br />\";\n\n                text = record.rData.text;\n            }\n\n            tableHtmlRow += \"<br />\";\n\n            additionalDataAttributes = \"data-record-text=\\\"\" + htmlEncode(text) + \"\\\" \" +\n                \"data-record-split-text=\\\"\" + htmlEncode(record.rData.splitText) + \"\\\" \";\n            break;\n\n        case \"RP\":\n            tableHtmlRow += \"<b>Mailbox: </b> \" + htmlEncode(record.rData.mailbox) +\n                \"<br /><b>TXT Domain:</b> \" + htmlEncode(record.rData.txtDomain);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-mailbox=\\\"\" + htmlEncode(record.rData.mailbox) + \"\\\" \" +\n                \"data-record-txt-domain=\\\"\" + htmlEncode(record.rData.txtDomain) + \"\\\" \";\n            break;\n\n        case \"SRV\":\n            tableHtmlRow += \"<b>Priority: </b> \" + htmlEncode(record.rData.priority) +\n                \"<br /><b>Weight:</b> \" + htmlEncode(record.rData.weight) +\n                \"<br /><b>Port:</b> \" + htmlEncode(record.rData.port) +\n                \"<br /><b>Target:</b> \" + htmlEncode(record.rData.target);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-priority=\\\"\" + htmlEncode(record.rData.priority) + \"\\\" \" +\n                \"data-record-weight=\\\"\" + htmlEncode(record.rData.weight) + \"\\\" \" +\n                \"data-record-port=\\\"\" + htmlEncode(record.rData.port) + \"\\\" \" +\n                \"data-record-target=\\\"\" + htmlEncode(record.rData.target) + \"\\\" \";\n            break;\n\n        case \"NAPTR\":\n            tableHtmlRow += \"<b>Order: </b> \" + htmlEncode(record.rData.order) +\n                \"<br /><b>Preference:</b> \" + htmlEncode(record.rData.preference) +\n                \"<br /><b>Flags:</b> \" + htmlEncode(record.rData.flags) +\n                \"<br /><b>Services:</b> \" + htmlEncode(record.rData.services) +\n                \"<br /><b>Regular Expression:</b> \" + htmlEncode(record.rData.regexp) +\n                \"<br /><b>Replacement:</b> \" + htmlEncode(record.rData.replacement);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-order=\\\"\" + htmlEncode(record.rData.order) + \"\\\" \" +\n                \"data-record-preference=\\\"\" + htmlEncode(record.rData.preference) + \"\\\" \" +\n                \"data-record-flags=\\\"\" + htmlEncode(record.rData.flags) + \"\\\" \" +\n                \"data-record-services=\\\"\" + htmlEncode(record.rData.services) + \"\\\" \" +\n                \"data-record-regexp=\\\"\" + htmlEncode(record.rData.regexp) + \"\\\" \" +\n                \"data-record-replacement=\\\"\" + htmlEncode(record.rData.replacement) + \"\\\" \";\n            break;\n\n        case \"DNAME\":\n            tableHtmlRow += htmlEncode(record.rData.dname);\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-dname=\\\"\" + htmlEncode(record.rData.dname) + \"\\\" \";\n            break;\n\n        case \"APL\":\n            tableHtmlRow += \"<table class=\\\"table\\\" style=\\\"background: transparent;\\\"><thead><tr><th>Family</th><th>Negation</th><th>AFD Part</th><th>Prefix</th></tr></thead><tbody>\";\n\n            for (var i = 0; i < record.rData.addressPrefixes.length; i++) {\n                tableHtmlRow += \"<tr><td>\" + record.rData.addressPrefixes[i].addressFamily + \"</td>\";\n                tableHtmlRow += \"<td>\" + record.rData.addressPrefixes[i].negation + \"</td>\";\n                tableHtmlRow += \"<td>\" + record.rData.addressPrefixes[i].afdPart + \"</td>\";\n                tableHtmlRow += \"<td>\" + record.rData.addressPrefixes[i].prefix + \"</td></tr>\";\n            }\n\n            tableHtmlRow += \"</tbody></table>\";\n\n            additionalDataAttributes = \"\";\n            break;\n\n        case \"DS\":\n            tableHtmlRow += \"<b>Key Tag: </b> \" + htmlEncode(record.rData.keyTag) +\n                \"<br /><b>Algorithm:</b> \" + htmlEncode(record.rData.algorithm + \" (\" + record.rData.algorithmNumber + \")\") +\n                \"<br /><b>Digest Type:</b> \" + htmlEncode(record.rData.digestType + \" (\" + record.rData.digestTypeNumber + \")\") +\n                \"<br /><b>Digest:</b> \" + htmlEncode(record.rData.digest);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-key-tag=\\\"\" + htmlEncode(record.rData.keyTag) + \"\\\" \" +\n                \"data-record-algorithm=\\\"\" + htmlEncode(record.rData.algorithm) + \"\\\" \" +\n                \"data-record-digest-type=\\\"\" + htmlEncode(record.rData.digestType) + \"\\\" \" +\n                \"data-record-digest=\\\"\" + htmlEncode(record.rData.digest) + \"\\\" \";\n            break;\n\n        case \"SSHFP\":\n            tableHtmlRow += \"<b>Algorithm:</b> \" + htmlEncode(record.rData.algorithm) +\n                \"<br /><b>Fingerprint Type:</b> \" + htmlEncode(record.rData.fingerprintType) +\n                \"<br /><b>Fingerprint:</b> \" + htmlEncode(record.rData.fingerprint);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-algorithm=\\\"\" + htmlEncode(record.rData.algorithm) + \"\\\" \" +\n                \"data-record-fingerprint-type=\\\"\" + htmlEncode(record.rData.fingerprintType) + \"\\\" \" +\n                \"data-record-fingerprint=\\\"\" + htmlEncode(record.rData.fingerprint) + \"\\\" \";\n            break;\n\n        case \"RRSIG\":\n            tableHtmlRow += \"<b>Type Covered: </b> \" + htmlEncode(record.rData.typeCovered) +\n                \"<br /><b>Algorithm:</b> \" + htmlEncode(record.rData.algorithm + \" (\" + record.rData.algorithmNumber + \")\") +\n                \"<br /><b>Labels:</b> \" + htmlEncode(record.rData.labels) +\n                \"<br /><b>Original TTL:</b> \" + htmlEncode(record.rData.originalTtl) +\n                \"<br /><b>Signature Expiration:</b> \" + moment(record.rData.signatureExpiration).local().format(\"YYYY-MM-DD HH:mm:ss\") +\n                \"<br /><b>Signature Inception:</b> \" + moment(record.rData.signatureInception).local().format(\"YYYY-MM-DD HH:mm:ss\") +\n                \"<br /><b>Key Tag:</b> \" + htmlEncode(record.rData.keyTag) +\n                \"<br /><b>Signer's Name:</b> \" + htmlEncode(record.rData.signersName) +\n                \"<br /><b>Signature:</b> \" + htmlEncode(record.rData.signature);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"\";\n            break;\n\n        case \"NSEC\":\n            var nsecTypes = null;\n\n            for (var j = 0; j < record.rData.types.length; j++) {\n                if (nsecTypes == null)\n                    nsecTypes = record.rData.types[j];\n                else\n                    nsecTypes += \", \" + record.rData.types[j];\n            }\n\n            tableHtmlRow += \"<b>Next Domain Name: </b> \" + htmlEncode(record.rData.nextDomainName) +\n                \"<br /><b>Types:</b> \" + htmlEncode(nsecTypes);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"\";\n            break;\n\n        case \"DNSKEY\":\n            tableHtmlRow += \"<b>Flags: </b> \" + htmlEncode(record.rData.flags) +\n                \"<br /><b>Protocol:</b> \" + htmlEncode(record.rData.protocol) +\n                \"<br /><b>Algorithm:</b> \" + htmlEncode(record.rData.algorithm + \" (\" + record.rData.algorithmNumber + \")\") +\n                \"<br /><b>Public Key:</b> \" + htmlEncode(record.rData.publicKey);\n\n            if (record.rData.dnsKeyState == null) {\n                tableHtmlRow += \"<br />\";\n            }\n            else {\n                if (record.rData.dnsKeyStateReadyBy != null)\n                    tableHtmlRow += \"<br /><br /><b>Key State:</b> \" + htmlEncode(record.rData.dnsKeyState) + \" (ready by: \" + moment(record.rData.dnsKeyStateReadyBy).local().format(\"YYYY-MM-DD HH:mm\") + \")\";\n                else if (record.rData.dnsKeyStateActiveBy != null)\n                    tableHtmlRow += \"<br /><br /><b>Key State:</b> \" + htmlEncode(record.rData.dnsKeyState) + \" (active by: \" + moment(record.rData.dnsKeyStateActiveBy).local().format(\"YYYY-MM-DD HH:mm\") + \")\";\n                else\n                    tableHtmlRow += \"<br /><br /><b>Key State:</b> \" + htmlEncode(record.rData.dnsKeyState);\n            }\n\n            tableHtmlRow += \"<br /><b>Computed Key Tag:</b> \" + htmlEncode(record.rData.computedKeyTag);\n\n            if (record.rData.computedDigests != null) {\n                tableHtmlRow += \"<br /><b>Computed Digests:</b> \";\n\n                for (var j = 0; j < record.rData.computedDigests.length; j++) {\n                    tableHtmlRow += \"<br />\" + htmlEncode(record.rData.computedDigests[j].digestType) + \": \" + htmlEncode(record.rData.computedDigests[j].digest)\n                }\n            }\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"\";\n            break;\n\n        case \"NSEC3\":\n            var nsec3Types = null;\n\n            for (var j = 0; j < record.rData.types.length; j++) {\n                if (nsec3Types == null)\n                    nsec3Types = record.rData.types[j];\n                else\n                    nsec3Types += \", \" + record.rData.types[j];\n            }\n\n            tableHtmlRow += \"<b>Hash Algorithm: </b> \" + htmlEncode(record.rData.hashAlgorithm) +\n                \"<br /><b>Flags: </b> \" + htmlEncode(record.rData.flags) +\n                \"<br /><b>Iterations: </b> \" + htmlEncode(record.rData.iterations) +\n                \"<br /><b>Salt: </b>\" + htmlEncode(record.rData.salt) +\n                \"<br /><b>Next Hashed Owner Name: </b> \" + htmlEncode(record.rData.nextHashedOwnerName) +\n                \"<br /><b>Types:</b> \" + htmlEncode(nsec3Types);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"\";\n            break;\n\n        case \"NSEC3PARAM\":\n            tableHtmlRow += \"<b>Hash Algorithm: </b> \" + htmlEncode(record.rData.hashAlgorithm) +\n                \"<br /><b>Flags: </b> \" + htmlEncode(record.rData.flags) +\n                \"<br /><b>Iterations: </b> \" + htmlEncode(record.rData.iterations) +\n                \"<br /><b>Salt: </b>\" + htmlEncode(record.rData.salt);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"\";\n            break;\n\n        case \"TLSA\":\n            tableHtmlRow += \"<b>Certificate Usage: </b> \" + htmlEncode(record.rData.certificateUsage) +\n                \"<br /><b>Selector: </b> \" + htmlEncode(record.rData.selector) +\n                \"<br /><b>Matching Type: </b> \" + htmlEncode(record.rData.matchingType) +\n                \"<br /><b>Certificate Association Data:</b> \" + (record.rData.certificateAssociationData == \"\" ? \"<br />\" : \"<pre style=\\\"white-space: pre-wrap;\\\">\" + htmlEncode(record.rData.certificateAssociationData) + \"</pre>\");\n\n            tableHtmlRow += \"<br />\";\n\n            additionalDataAttributes = \"data-record-certificate-usage=\\\"\" + htmlEncode(record.rData.certificateUsage) + \"\\\" \" +\n                \"data-record-selector=\\\"\" + htmlEncode(record.rData.selector) + \"\\\" \" +\n                \"data-record-matching-type=\\\"\" + htmlEncode(record.rData.matchingType) + \"\\\" \" +\n                \"data-record-certificate-association-data=\\\"\" + htmlEncode(record.rData.certificateAssociationData) + \"\\\" \";\n            break;\n\n        case \"ZONEMD\":\n            tableHtmlRow += \"<b>Serial: </b> \" + htmlEncode(record.rData.serial) +\n                \"<br /><b>Scheme: </b> \" + htmlEncode(record.rData.scheme) +\n                \"<br /><b>Hash Algorithm: </b> \" + htmlEncode(record.rData.hashAlgorithm) +\n                \"<br /><b>Digest:</b> \" + record.rData.digest;\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"\";\n            break;\n\n        case \"SVCB\":\n        case \"HTTPS\":\n            var tableHtmlSvcParams;\n\n            if (Object.keys(record.rData.svcParams).length == 0) {\n                tableHtmlSvcParams = \"<br />\";\n            }\n            else {\n                tableHtmlSvcParams = \"<br /><b>Params: </b><table class=\\\"table table-condensed\\\" style=\\\"background: transparent; margin-bottom: 0px;\\\">\" +\n                    \"<thead><tr>\" +\n                    \"<th>Key</th>\" +\n                    \"<th>Value</th>\" +\n                    \"</thead>\" +\n                    \"<tbody>\";\n\n                for (var paramKey in record.rData.svcParams) {\n                    switch (paramKey) {\n                        case \"ipv4hint\":\n                            if (record.rData.autoIpv4Hint)\n                                continue;\n\n                            break;\n\n                        case \"ipv6hint\":\n                            if (record.rData.autoIpv6Hint)\n                                continue;\n\n                            break;\n                    }\n\n                    tableHtmlSvcParams += \"<tr><td>\" + htmlEncode(paramKey) + \"</td><td>\" + htmlEncode(record.rData.svcParams[paramKey]) + \"</td></tr>\";\n                }\n\n                tableHtmlSvcParams += \"</tbody></table>\";\n            }\n\n            tableHtmlRow += \"<b>Priority: </b> \" + htmlEncode(record.rData.svcPriority) + (record.rData.svcPriority == 0 ? \" (alias mode)\" : \" (service mode)\") +\n                \"<br /><b>Target Name: </b> \" + (record.rData.svcTargetName == \"\" ? \".\" : htmlEncode(record.rData.svcTargetName)) +\n                tableHtmlSvcParams +\n                \"<br /><b>Use Automatic IPv4 Hint: </b> \" + record.rData.autoIpv4Hint +\n                \"<br /><b>Use Automatic IPv6 Hint: </b> \" + record.rData.autoIpv6Hint +\n                \"<br />\";\n\n            tableHtmlRow += \"<br />\";\n\n            additionalDataAttributes = \"data-record-svc-priority=\\\"\" + htmlEncode(record.rData.svcPriority) + \"\\\"\" +\n                \"data-record-svc-target-name=\\\"\" + (record.rData.svcTargetName == \"\" ? \".\" : htmlEncode(record.rData.svcTargetName)) + \"\\\"\" +\n                \"data-record-svc-params=\\\"\" + htmlEncode(JSON.stringify(record.rData.svcParams)) + \"\\\"\" +\n                \"data-record-auto-ipv4hint=\\\"\" + htmlEncode(record.rData.autoIpv4Hint) + \"\\\"\" +\n                \"data-record-auto-ipv6hint=\\\"\" + htmlEncode(record.rData.autoIpv6Hint) + \"\\\"\";\n            break;\n\n        case \"URI\":\n            tableHtmlRow += \"<b>Priority: </b> \" + htmlEncode(record.rData.priority) +\n                \"<br /><b>Weight:</b> \" + htmlEncode(record.rData.weight) +\n                \"<br /><b>URI:</b> \" + htmlEncode(record.rData.uri);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-priority=\\\"\" + htmlEncode(record.rData.priority) + \"\\\" \" +\n                \"data-record-weight=\\\"\" + htmlEncode(record.rData.weight) + \"\\\" \" +\n                \"data-record-uri=\\\"\" + htmlEncode(record.rData.uri) + \"\\\" \";\n            break;\n\n        case \"CAA\":\n            tableHtmlRow += \"<b>Flags: </b> \" + htmlEncode(record.rData.flags) +\n                \"<br /><b>Tag:</b> \" + htmlEncode(record.rData.tag) +\n                \"<br /><b>Authority:</b> \" + htmlEncode(record.rData.value);\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-flags=\\\"\" + htmlEncode(record.rData.flags) + \"\\\" \" +\n                \"data-record-tag=\\\"\" + htmlEncode(record.rData.tag) + \"\\\" \" +\n                \"data-record-value=\\\"\" + htmlEncode(record.rData.value) + \"\\\" \";\n            break;\n\n        case \"ANAME\":\n            tableHtmlRow += \"\" + htmlEncode(record.rData.aname);\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-aname=\\\"\" + htmlEncode(record.rData.aname) + \"\\\" \";\n            break;\n\n        case \"FWD\":\n            tableHtmlRow += \"<b>Protocol: </b> \" + htmlEncode(record.rData.protocol) +\n                \"<br /><b>Forwarder:</b> \" + htmlEncode(record.rData.forwarder) +\n                \"<br /><b>Priority:</b> \" + htmlEncode(record.rData.priority) +\n                \"<br /><b>Enable DNSSEC Validation:</b> \" + htmlEncode(record.rData.dnssecValidation) +\n                \"<br /><b>Proxy Type:</b> \" + htmlEncode(record.rData.proxyType);\n\n            switch (record.rData.proxyType) {\n                case \"Http\":\n                case \"Socks5\":\n                    tableHtmlRow += \"<br /><b>Proxy Address:</b> \" + htmlEncode(record.rData.proxyAddress) +\n                        \"<br /><b>Proxy Port:</b> \" + htmlEncode(record.rData.proxyPort) +\n                        \"<br /><b>Proxy Username:</b> \" + htmlEncode(record.rData.proxyUsername) +\n                        \"<br /><b>Proxy Password:</b> ************\";\n                    break;\n            }\n\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-protocol=\\\"\" + htmlEncode(record.rData.protocol) + \"\\\" \" +\n                \"data-record-forwarder=\\\"\" + htmlEncode(record.rData.forwarder) + \"\\\" \" +\n                \"data-record-priority=\\\"\" + htmlEncode(record.rData.priority) + \"\\\" \" +\n                \"data-record-dnssec-validation=\\\"\" + htmlEncode(record.rData.dnssecValidation) + \"\\\" \" +\n                \"data-record-proxy-type=\\\"\" + htmlEncode(record.rData.proxyType) + \"\\\" \";\n\n            switch (record.rData.proxyType) {\n                case \"Http\":\n                case \"Socks5\":\n                    additionalDataAttributes += \"data-record-proxy-address=\\\"\" + htmlEncode(record.rData.proxyAddress) + \"\\\" \" +\n                        \"data-record-proxy-port=\\\"\" + htmlEncode(record.rData.proxyPort) + \"\\\" \" +\n                        \"data-record-proxy-username=\\\"\" + htmlEncode(record.rData.proxyUsername) + \"\\\" \" +\n                        \"data-record-proxy-password=\\\"\" + htmlEncode(record.rData.proxyPassword) + \"\\\" \";\n                    break;\n            }\n            break;\n\n        case \"APP\":\n            tableHtmlRow += \"<b>App Name: </b> \" + htmlEncode(record.rData.appName) +\n                \"<br /><b>Class Path:</b> \" + htmlEncode(record.rData.classPath) +\n                \"<br /><b>Record Data:</b> \" + (record.rData.data == \"\" ? \"<br />\" : \"<pre style=\\\"white-space: pre-wrap;\\\">\" + htmlEncode(record.rData.data) + \"</pre>\");\n\n            tableHtmlRow += \"<br />\";\n\n            additionalDataAttributes = \"data-record-app-name=\\\"\" + htmlEncode(record.rData.appName) + \"\\\" \" +\n                \"data-record-classpath=\\\"\" + htmlEncode(record.rData.classPath) + \"\\\" \" +\n                \"data-record-data=\\\"\" + htmlEncode(record.rData.data) + \"\\\"\";\n            break;\n\n        case \"ALIAS\":\n            tableHtmlRow += \"<b>Type: </b> \" + htmlEncode(record.rData.type) +\n                \"<br /><b>Alias:</b> \" + htmlEncode(record.rData.alias);\n\n            tableHtmlRow += \"<br /><br />\";\n            break;\n\n        default:\n            tableHtmlRow += \"<b>RDATA:</b> \" + htmlEncode(record.rData.value);\n            tableHtmlRow += \"<br /><br />\";\n\n            additionalDataAttributes = \"data-record-rdata=\\\"\" + htmlEncode(record.rData.value) + \"\\\"\";\n            break;\n    }\n\n    if (record.expiryTtl > 0) {\n        var expiresOn = moment(record.lastModified).add(record.expiryTtl, \"s\");\n        tableHtmlRow += \"<b>Expiry TTL:</b> \" + record.expiryTtl + \" (\" + record.expiryTtlString + \")\";\n        tableHtmlRow += \"<br /><b>Expires On:</b> \" + expiresOn.local().format(\"YYYY-MM-DD HH:mm:ss\") + \" (\" + expiresOn.fromNow() + \")\";\n        tableHtmlRow += \"<br />\";\n    }\n\n    var lastUsedOn;\n\n    if (record.lastUsedOn == \"0001-01-01T00:00:00\")\n        lastUsedOn = moment(record.lastUsedOn).local().format(\"YYYY-MM-DD HH:mm:ss\") + \" (never)\";\n    else\n        lastUsedOn = moment(record.lastUsedOn).local().format(\"YYYY-MM-DD HH:mm:ss\") + \" (\" + moment(record.lastUsedOn).fromNow() + \")\";\n\n    tableHtmlRow += \"<b>Last Used:</b> \" + lastUsedOn;\n\n    if ((record.lastModified != \"0001-01-01T00:00:00\") && (record.lastModified != \"0001-01-01T00:00:00Z\"))\n        tableHtmlRow += \"<br /><b>Last Modified:</b> \" + moment(record.lastModified).local().format(\"YYYY-MM-DD HH:mm:ss\") + \" (\" + moment(record.lastModified).fromNow() + \")\";;\n\n    if ((record.comments != null) && (record.comments.length > 0))\n        tableHtmlRow += \"<br /><b>Comments:</b> <pre style=\\\"white-space: pre-wrap;\\\">\" + htmlEncode(record.comments) + \"</pre>\";\n\n    tableHtmlRow += \"</td>\";\n\n    var hideActionButtons = false;\n    var disableEnableDisableDeleteButtons = false;\n\n    switch (zoneType) {\n        case \"Internal\":\n        case \"Secondary\":\n        case \"SecondaryForwarder\":\n        case \"SecondaryCatalog\":\n        case \"Stub\":\n            hideActionButtons = true;\n            break;\n\n        case \"Catalog\":\n            switch (record.type) {\n                case \"SOA\":\n                    disableEnableDisableDeleteButtons = true;\n                    break;\n\n                default:\n                    hideActionButtons = true;\n                    break;\n            }\n            break;\n\n        default:\n            switch (record.type) {\n                case \"SOA\":\n                    disableEnableDisableDeleteButtons = true;\n                    break;\n\n                case \"DNSKEY\":\n                case \"RRSIG\":\n                case \"NSEC\":\n                case \"NSEC3\":\n                case \"NSEC3PARAM\":\n                case \"ZONEMD\":\n                    hideActionButtons = true;\n                    break;\n            }\n            break;\n    }\n\n    if (hideActionButtons) {\n        tableHtmlRow += \"<td align=\\\"right\\\">&nbsp;</td>\";\n    }\n    else {\n        tableHtmlRow += \"<td align=\\\"right\\\" style=\\\"min-width: 220px;\\\">\";\n        tableHtmlRow += \"<div id=\\\"data\" + index + \"\\\" data-record-index=\\\"\" + (record.index == null ? index : record.index) + \"\\\" data-record-name=\\\"\" + htmlEncode(record.name) + \"\\\" data-record-type=\\\"\" + record.type + \"\\\" data-record-ttl=\\\"\" + record.ttl + \"\\\" \" + additionalDataAttributes + \" data-record-disabled=\\\"\" + record.disabled + \"\\\" data-record-comments=\\\"\" + htmlEncode(record.comments) + \"\\\" data-record-expiry-ttl=\\\"\" + record.expiryTtl + \"\\\" style=\\\"display: none;\\\"></div>\";\n        tableHtmlRow += \"<button type=\\\"button\\\" class=\\\"btn btn-primary\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 0 0;\\\" data-id=\\\"\" + index + \"\\\" onclick=\\\"showEditRecordModal(this);\\\">Edit</button>\";\n        tableHtmlRow += \"<button type=\\\"button\\\" class=\\\"btn btn-default\\\" id=\\\"btnEnableRecord\" + index + \"\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 0 0;\" + (record.disabled ? \"\" : \" display: none;\") + \"\\\" data-id=\\\"\" + index + \"\\\" onclick=\\\"updateRecordState(this, false);\\\"\" + (disableEnableDisableDeleteButtons ? \" disabled\" : \"\") + \" data-loading-text=\\\"Enabling...\\\">Enable</button>\";\n        tableHtmlRow += \"<button type=\\\"button\\\" class=\\\"btn btn-warning\\\" id=\\\"btnDisableRecord\" + index + \"\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 0 0;\" + (!record.disabled ? \"\" : \" display: none;\") + \"\\\" data-id=\\\"\" + index + \"\\\" onclick=\\\"updateRecordState(this, true);\\\"\" + (disableEnableDisableDeleteButtons ? \" disabled\" : \"\") + \" data-loading-text=\\\"Disabling...\\\">Disable</button>\";\n        tableHtmlRow += \"<button type=\\\"button\\\" class=\\\"btn btn-danger\\\" style=\\\"font-size: 12px; padding: 2px 0px; width: 60px; margin: 0 6px 0 0;\\\" data-loading-text=\\\"Deleting...\\\" data-id=\\\"\" + index + \"\\\" onclick=\\\"deleteRecord(this);\\\"\" + (disableEnableDisableDeleteButtons ? \" disabled\" : \"\") + \">Delete</button></td>\";\n    }\n\n    tableHtmlRow += \"</tr>\";\n\n    return tableHtmlRow;\n}\n\nfunction clearAddEditRecordForm() {\n    $(\"#divAddEditRecordAlert\").html(\"\");\n\n    $(\"#txtAddEditRecordName\").prop(\"placeholder\", \"@\");\n    $(\"#txtAddEditRecordName\").prop(\"disabled\", false);\n    $(\"#optAddEditRecordType\").prop(\"disabled\", false);\n    $(\"#txtAddEditRecordTtl\").prop(\"disabled\", false);\n\n    $(\"#txtAddEditRecordName\").val(\"\");\n    $(\"#optAddEditRecordType\").val(\"A\");\n    $(\"#txtAddEditRecordTtl\").val(\"\");\n    $(\"#txtAddEditRecordTtl\").attr(\"placeholder\", sessionData.info.defaultRecordTtl);\n    $(\"#spanAddEditRecordTtlUnit\").text(\"seconds (default \" + sessionData.info.defaultRecordTtl + \")\");\n\n    $(\"#divAddEditRecordData\").show();\n    $(\"#divAddEditRecordDataUnknownType\").hide();\n    $(\"#txtAddEditRecordDataUnknownType\").val(\"\");\n    $(\"#txtAddEditRecordDataUnknownType\").prop(\"disabled\", false);\n    $(\"#lblAddEditRecordDataValue\").text(\"IPv4 Address\");\n    $(\"#txtAddEditRecordDataValue\").val(\"\");\n    $(\"#divAddEditRecordDataPtr\").show();\n    $(\"#chkAddEditRecordDataPtr\").prop(\"checked\", false);\n    $(\"#chkAddEditRecordDataCreatePtrZone\").prop(\"disabled\", true);\n    $(\"#chkAddEditRecordDataCreatePtrZone\").prop(\"checked\", false);\n    $(\"#chkAddEditRecordDataPtrLabel\").text(\"Add reverse (PTR) record\");\n\n    $(\"#divAddEditRecordDataNs\").hide();\n    $(\"#txtAddEditRecordDataNsNameServer\").prop(\"disabled\", false);\n    $(\"#txtAddEditRecordDataNsNameServer\").val(\"\");\n    $(\"#txtAddEditRecordDataNsGlue\").prop(\"disabled\", false);\n    $(\"#txtAddEditRecordDataNsGlue\").val(\"\");\n\n    $(\"#divEditRecordDataSoa\").hide();\n    $(\"#txtEditRecordDataSoaPrimaryNameServer\").prop(\"disabled\", false);\n    $(\"#txtEditRecordDataSoaResponsiblePerson\").prop(\"disabled\", false);\n    $(\"#txtEditRecordDataSoaSerial\").prop(\"disabled\", false);\n    $(\"#txtEditRecordDataSoaRefresh\").prop(\"disabled\", false);\n    $(\"#txtEditRecordDataSoaRetry\").prop(\"disabled\", false);\n    $(\"#txtEditRecordDataSoaExpire\").prop(\"disabled\", false);\n    $(\"#txtEditRecordDataSoaMinimum\").prop(\"disabled\", false);\n    $(\"#txtEditRecordDataSoaPrimaryNameServer\").val(\"\");\n    $(\"#txtEditRecordDataSoaResponsiblePerson\").val(\"\");\n    $(\"#txtEditRecordDataSoaSerial\").val(\"\");\n    $(\"#txtEditRecordDataSoaRefresh\").val(\"\");\n    $(\"#txtEditRecordDataSoaRetry\").val(\"\");\n    $(\"#txtEditRecordDataSoaExpire\").val(\"\");\n    $(\"#txtEditRecordDataSoaMinimum\").val(\"\");\n\n    $(\"#divAddEditRecordDataMx\").hide();\n    $(\"#txtAddEditRecordDataMxPreference\").val(\"\");\n    $(\"#txtAddEditRecordDataMxExchange\").val(\"\");\n\n    $(\"#divAddEditRecordDataTxt\").hide();\n    $(\"#txtAddEditRecordDataTxt\").val(\"\");\n    $(\"#chkAddEditRecordDataTxtSplitText\").prop(\"checked\", false);\n\n    $(\"#divAddEditRecordDataSrv\").hide();\n    $(\"#txtAddEditRecordDataSrvPriority\").val(\"\");\n    $(\"#txtAddEditRecordDataSrvWeight\").val(\"\");\n    $(\"#txtAddEditRecordDataSrvPort\").val(\"\");\n    $(\"#txtAddEditRecordDataSrvTarget\").val(\"\");\n\n    $(\"#divAddEditRecordDataNaptr\").hide();\n    $(\"#txtAddEditRecordDataNaptrOrder\").val(\"\");\n    $(\"#txtAddEditRecordDataNaptrPreference\").val(\"\");\n    $(\"#txtAddEditRecordDataNaptrFlags\").val(\"\");\n    $(\"#txtAddEditRecordDataNaptrServices\").val(\"\");\n    $(\"#txtAddEditRecordDataNaptrRegExp\").val(\"\");\n    $(\"#txtAddEditRecordDataNaptrReplacement\").val(\"\");\n\n    $(\"#divAddEditRecordDataDs\").hide();\n    $(\"#txtAddEditRecordDataDsKeyTag\").val(\"\");\n    $(\"#optAddEditRecordDataDsAlgorithm\").val(\"\");\n    $(\"#optAddEditRecordDataDsDigestType\").val(\"\");\n    $(\"#txtAddEditRecordDataDsDigest\").val(\"\");\n\n    $(\"#divAddEditRecordDataSshfp\").hide();\n    $(\"#optAddEditRecordDataSshfpAlgorithm\").val(\"\");\n    $(\"#optAddEditRecordDataSshfpFingerprintType\").val(\"\");\n    $(\"#txtAddEditRecordDataSshfpFingerprint\").val(\"\");\n\n    $(\"#divAddEditRecordDataTlsa\").hide();\n    $(\"#optAddEditRecordDataTlsaCertificateUsage\").val(\"\");\n    $(\"#optAddEditRecordDataTlsaSelector\").val(\"\");\n    $(\"#optAddEditRecordDataTlsaMatchingType\").val(\"\");\n    $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").val(\"\");\n\n    $(\"#divAddEditRecordDataSvcb\").hide();\n    $(\"#txtAddEditRecordDataSvcbPriority\").val(\"\");\n    $(\"#txtAddEditRecordDataSvcbTargetName\").val(\"\");\n    $(\"#tableAddEditRecordDataSvcbParams\").html(\"\");\n    $(\"#chkAddEditRecordDataSvcbAutoIpv4Hint\").prop(\"checked\", false);\n    $(\"#chkAddEditRecordDataSvcbAutoIpv6Hint\").prop(\"checked\", false);\n\n    $(\"#divAddEditRecordDataUri\").hide();\n    $(\"#txtAddEditRecordDataUriPriority\").val(\"\");\n    $(\"#txtAddEditRecordDataUriWeight\").val(\"\");\n    $(\"#txtAddEditRecordDataUri\").val(\"\");\n\n    $(\"#divAddEditRecordDataCaa\").hide();\n    $(\"#txtAddEditRecordDataCaaFlags\").val(\"\");\n    $(\"#txtAddEditRecordDataCaaTag\").val(\"\");\n    $(\"#txtAddEditRecordDataCaaValue\").val(\"\");\n\n    $(\"#divAddEditRecordDataForwarder\").hide();\n    $(\"#rdAddEditRecordDataForwarderProtocolUdp\").prop(\"checked\", true);\n    $(\"input[name=rdAddEditRecordDataForwarderProtocol]:radio\").attr(\"disabled\", false);\n    $(\"#chkAddEditRecordDataForwarderThisServer\").prop(\"checked\", false);\n    $('#txtAddEditRecordDataForwarder').prop(\"disabled\", false);\n    $(\"#txtAddEditRecordDataForwarder\").attr(\"placeholder\", \"8.8.8.8 or [2620:fe::10]\")\n    $(\"#txtAddEditRecordDataForwarder\").val(\"\");\n    $(\"#txtAddEditRecordDataForwarderPriority\").val(\"\");\n    $(\"#chkAddEditRecordDataForwarderDnssecValidation\").prop(\"checked\", $(\"#chkDnssecValidation\").prop(\"checked\"));\n    $(\"#rdAddEditRecordDataForwarderProxyTypeDefaultProxy\").prop(\"checked\", true);\n    $(\"#txtAddEditRecordDataForwarderProxyAddress\").prop(\"disabled\", true);\n    $(\"#txtAddEditRecordDataForwarderProxyPort\").prop(\"disabled\", true);\n    $(\"#txtAddEditRecordDataForwarderProxyUsername\").prop(\"disabled\", true);\n    $(\"#txtAddEditRecordDataForwarderProxyPassword\").prop(\"disabled\", true);\n    $(\"#txtAddEditRecordDataForwarderProxyAddress\").val(\"\");\n    $(\"#txtAddEditRecordDataForwarderProxyPort\").val(\"\");\n    $(\"#txtAddEditRecordDataForwarderProxyUsername\").val(\"\");\n    $(\"#txtAddEditRecordDataForwarderProxyPassword\").val(\"\");\n\n    $(\"#divAddEditRecordDataApplication\").hide();\n    $(\"#optAddEditRecordDataAppName\").html(\"\");\n    $(\"#optAddEditRecordDataAppName\").prop(\"disabled\", false);\n    $(\"#optAddEditRecordDataClassPath\").html(\"\");\n    $(\"#optAddEditRecordDataClassPath\").prop(\"disabled\", false);\n    $(\"#txtAddEditRecordDataData\").val(\"\");\n\n    $(\"#divAddEditRecordOverwrite\").show();\n    $(\"#chkAddEditRecordOverwrite\").prop(\"checked\", false);\n\n    $(\"#txtAddEditRecordComments\").val(\"\");\n\n    $(\"#divAddEditRecordExpiryTtl\").show();\n    $(\"#txtAddEditRecordExpiryTtl\").prop(\"disabled\", false);\n    $(\"#txtAddEditRecordExpiryTtl\").val(\"\");\n\n    $(\"#btnAddEditRecord\").button(\"reset\");\n}\n\nfunction showAddRecordModal() {\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n\n    var lastType = $(\"#optAddEditRecordType\").val();\n\n    clearAddEditRecordForm();\n\n    if (zone.endsWith(\".in-addr.arpa\") || zone.endsWith(\".ip6.arpa\")) {\n        $(\"#optAddEditRecordType\").val(\"PTR\");\n        modifyAddRecordFormByType(true);\n    }\n    else if (lastType != \"SOA\") {\n        $(\"#optAddEditRecordType\").val(lastType);\n        modifyAddRecordFormByType(true);\n    }\n\n    $(\"#titleAddEditRecord\").text(\"Add Record\");\n    $(\"#lblAddEditRecordZoneName\").text(zone === \".\" ? \"\" : zone);\n    $(\"#optEditRecordTypeSoa\").hide();\n    $(\"#btnAddEditRecord\").attr(\"onclick\", \"addRecord(); return false;\");\n\n    $(\"#modalAddEditRecord\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtAddEditRecordName\").trigger(\"focus\");\n    }, 1000);\n}\n\nvar appsList;\n\nfunction loadAddRecordModalAppNames() {\n    var optAddEditRecordDataAppName = $(\"#optAddEditRecordDataAppName\");\n    var optAddEditRecordDataClassPath = $(\"#optAddEditRecordDataClassPath\");\n    var txtAddEditRecordDataData = $(\"#txtAddEditRecordDataData\");\n    var divAddEditRecordAlert = $(\"#divAddEditRecordAlert\");\n\n    optAddEditRecordDataAppName.prop(\"disabled\", true);\n    optAddEditRecordDataClassPath.prop(\"disabled\", true);\n    txtAddEditRecordDataData.prop(\"disabled\", true);\n\n    optAddEditRecordDataAppName.html(\"\");\n    optAddEditRecordDataClassPath.html(\"\");\n    txtAddEditRecordDataData.val(\"\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    HTTPRequest({\n        url: \"api/apps/list?token=\" + sessionData.token + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            appsList = responseJSON.response.apps;\n\n            var optApps = \"<option></option>\";\n            var optClassPaths = \"<option></option>\";\n\n            for (var i = 0; i < appsList.length; i++) {\n                for (var j = 0; j < appsList[i].dnsApps.length; j++) {\n                    if (appsList[i].dnsApps[j].isAppRecordRequestHandler) {\n                        optApps += \"<option>\" + appsList[i].name + \"</option>\";\n                        break;\n                    }\n                }\n            }\n\n            $(\"#optAddEditRecordDataAppName\").html(optApps);\n            $(\"#optAddEditRecordDataClassPath\").html(optClassPaths);\n\n            optAddEditRecordDataAppName.prop(\"disabled\", false);\n            optAddEditRecordDataClassPath.prop(\"disabled\", false);\n            txtAddEditRecordDataData.prop(\"disabled\", false);\n        },\n        invalidToken: function () {\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAddEditRecordAlert\n    });\n}\n\nfunction modifyAddRecordFormByType(addMode) {\n    $(\"#divAddEditRecordAlert\").html(\"\");\n\n    $(\"#txtAddEditRecordName\").prop(\"placeholder\", \"@\");\n    $(\"#txtAddEditRecordTtl\").prop(\"disabled\", false);\n    $(\"#txtAddEditRecordTtl\").val(\"\");\n    $(\"#txtAddEditRecordTtl\").attr(\"placeholder\", sessionData.info.defaultRecordTtl);\n    $(\"#spanAddEditRecordTtlUnit\").text(\"seconds (default \" + sessionData.info.defaultRecordTtl + \")\");\n    $(\"#txtAddEditRecordDataValue\").attr(\"placeholder\", \"\");\n\n    var type = $(\"#optAddEditRecordType\").val();\n\n    $(\"#divAddEditRecordData\").hide();\n    $(\"#divAddEditRecordDataUnknownType\").hide();\n    $(\"#divAddEditRecordDataPtr\").hide();\n    $(\"#divAddEditRecordDataNs\").hide();\n    $(\"#divEditRecordDataSoa\").hide();\n    $(\"#divAddEditRecordDataMx\").hide();\n    $(\"#divAddEditRecordDataTxt\").hide();\n    $(\"#divAddEditRecordDataRp\").hide();\n    $(\"#divAddEditRecordDataSrv\").hide();\n    $(\"#divAddEditRecordDataNaptr\").hide();\n    $(\"#divAddEditRecordDataDs\").hide();\n    $(\"#divAddEditRecordDataSshfp\").hide();\n    $(\"#divAddEditRecordDataTlsa\").hide();\n    $(\"#divAddEditRecordDataSvcb\").hide();\n    $(\"#divAddEditRecordDataUri\").hide();\n    $(\"#divAddEditRecordDataCaa\").hide();\n    $(\"#divAddEditRecordDataForwarder\").hide();\n    $(\"#divAddEditRecordDataApplication\").hide();\n\n    switch (type) {\n        case \"A\":\n            $(\"#lblAddEditRecordDataValue\").text(\"IPv4 Address\");\n            $(\"#txtAddEditRecordDataValue\").val(\"\");\n            $(\"#chkAddEditRecordDataPtr\").prop(\"checked\", false);\n            $(\"#chkAddEditRecordDataCreatePtrZone\").prop('disabled', true);\n            $(\"#chkAddEditRecordDataCreatePtrZone\").prop(\"checked\", false);\n            $(\"#chkAddEditRecordDataPtrLabel\").text(\"Add reverse (PTR) record\");\n            $(\"#divAddEditRecordData\").show();\n            $(\"#divAddEditRecordDataPtr\").show();\n            break;\n\n        case \"AAAA\":\n            $(\"#lblAddEditRecordDataValue\").text(\"IPv6 Address\");\n            $(\"#txtAddEditRecordDataValue\").val(\"\");\n            $(\"#chkAddEditRecordDataPtr\").prop(\"checked\", false);\n            $(\"#chkAddEditRecordDataCreatePtrZone\").prop('disabled', true);\n            $(\"#chkAddEditRecordDataCreatePtrZone\").prop(\"checked\", false);\n            $(\"#chkAddEditRecordDataPtrLabel\").text(\"Add reverse (PTR) record\");\n            $(\"#divAddEditRecordData\").show();\n            $(\"#divAddEditRecordDataPtr\").show();\n            break;\n\n        case \"NS\":\n            $(\"#txtAddEditRecordDataNsNameServer\").val(\"\");\n            $(\"#txtAddEditRecordDataNsGlue\").val(\"\");\n            $(\"#divAddEditRecordDataNs\").show();\n            $(\"#txtAddEditRecordTtl\").attr(\"placeholder\", sessionData.info.defaultNsRecordTtl);\n            $(\"#spanAddEditRecordTtlUnit\").text(\"seconds (default \" + sessionData.info.defaultNsRecordTtl + \")\");\n            break;\n\n        case \"SOA\":\n            $(\"#txtEditRecordDataSoaPrimaryNameServer\").val(\"\");\n            $(\"#txtEditRecordDataSoaResponsiblePerson\").val(\"\");\n            $(\"#txtEditRecordDataSoaSerial\").val(\"\");\n            $(\"#txtEditRecordDataSoaRefresh\").val(\"\");\n            $(\"#txtEditRecordDataSoaRetry\").val(\"\");\n            $(\"#txtEditRecordDataSoaExpire\").val(\"\");\n            $(\"#txtEditRecordDataSoaMinimum\").val(\"\");\n            $(\"#divEditRecordDataSoa\").show();\n            $(\"#txtAddEditRecordTtl\").attr(\"placeholder\", sessionData.info.defaultSoaRecordTtl);\n            $(\"#spanAddEditRecordTtlUnit\").text(\"seconds (default \" + sessionData.info.defaultSoaRecordTtl + \")\");\n            break;\n\n        case \"PTR\":\n        case \"CNAME\":\n        case \"DNAME\":\n        case \"ANAME\":\n            $(\"#lblAddEditRecordDataValue\").text(\"Domain Name\");\n            $(\"#txtAddEditRecordDataValue\").val(\"\");\n            $(\"#divAddEditRecordData\").show();\n            break;\n\n        case \"MX\":\n            $(\"#txtAddEditRecordDataMxPreference\").val(\"\");\n            $(\"#txtAddEditRecordDataMxExchange\").val(\"\");\n            $(\"#divAddEditRecordDataMx\").show();\n            break;\n\n        case \"TXT\":\n            $(\"#txtAddEditRecordDataTxt\").val(\"\");\n            $(\"#chkAddEditRecordDataTxtSplitText\").prop(\"checked\", false);\n            $(\"#divAddEditRecordDataTxt\").show();\n            break;\n\n        case \"RP\":\n            $(\"#txtAddEditRecordDataRpMailbox\").val(\"\");\n            $(\"#txtAddEditRecordDataRpTxtDomain\").val(\"\");\n            $(\"#divAddEditRecordDataRp\").show();\n            break;\n\n        case \"SRV\":\n            $(\"#txtAddEditRecordName\").prop(\"placeholder\", \"_service._protocol.name\");\n            $(\"#txtAddEditRecordDataSrvPriority\").val(\"\");\n            $(\"#txtAddEditRecordDataSrvWeight\").val(\"\");\n            $(\"#txtAddEditRecordDataSrvPort\").val(\"\");\n            $(\"#txtAddEditRecordDataSrvTarget\").val(\"\");\n            $(\"#divAddEditRecordDataSrv\").show();\n            break;\n\n        case \"NAPTR\":\n            $(\"#txtAddEditRecordDataNaptrOrder\").val(\"\");\n            $(\"#txtAddEditRecordDataNaptrPreference\").val(\"\");\n            $(\"#txtAddEditRecordDataNaptrFlags\").val(\"\");\n            $(\"#txtAddEditRecordDataNaptrServices\").val(\"\");\n            $(\"#txtAddEditRecordDataNaptrRegExp\").val(\"\");\n            $(\"#txtAddEditRecordDataNaptrReplacement\").val(\"\");\n            $(\"#divAddEditRecordDataNaptr\").show();\n            break;\n\n        case \"DS\":\n            $(\"#txtAddEditRecordDataDsKeyTag\").val(\"\");\n            $(\"#optAddEditRecordDataDsAlgorithm\").val(\"\");\n            $(\"#optAddEditRecordDataDsDigestType\").val(\"\");\n            $(\"#txtAddEditRecordDataDsDigest\").val(\"\");\n            $(\"#divAddEditRecordDataDs\").show();\n            break;\n\n        case \"SSHFP\":\n            $(\"#optAddEditRecordDataSshfpAlgorithm\").val(\"\");\n            $(\"#optAddEditRecordDataSshfpFingerprintType\").val(\"\");\n            $(\"#txtAddEditRecordDataSshfpFingerprint\").val(\"\");\n            $(\"#divAddEditRecordDataSshfp\").show();\n            break;\n\n        case \"TLSA\":\n            $(\"#txtAddEditRecordName\").prop(\"placeholder\", \"_port._protocol.name\");\n            $(\"#optAddEditRecordDataTlsaCertificateUsage\").val(\"\");\n            $(\"#optAddEditRecordDataTlsaSelector\").val(\"\");\n            $(\"#optAddEditRecordDataTlsaMatchingType\").val(\"\");\n            $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").val(\"\");\n            $(\"#divAddEditRecordDataTlsa\").show();\n            break;\n\n        case \"SVCB\":\n        case \"HTTPS\":\n            $(\"#txtAddEditRecordName\").prop(\"placeholder\", \"_port._scheme.name\");\n            $(\"#txtAddEditRecordDataSvcbPriority\").val(\"\");\n            $(\"#txtAddEditRecordDataSvcbTargetName\").val(\"\");\n            $(\"#tableAddEditRecordDataSvcbParams\").html(\"\");\n            $(\"#chkAddEditRecordDataSvcbAutoIpv4Hint\").prop(\"checked\", false);\n            $(\"#chkAddEditRecordDataSvcbAutoIpv6Hint\").prop(\"checked\", false);\n            $(\"#divAddEditRecordDataSvcb\").show();\n            break;\n\n        case \"URI\":\n            $(\"#txtAddEditRecordDataUriPriority\").val(\"\");\n            $(\"#txtAddEditRecordDataUriWeight\").val(\"\");\n            $(\"#txtAddEditRecordDataUri\").val(\"\");\n            $(\"#divAddEditRecordDataUri\").show();\n            break;\n\n        case \"CAA\":\n            $(\"#txtAddEditRecordDataCaaFlags\").val(\"\");\n            $(\"#txtAddEditRecordDataCaaTag\").val(\"\");\n            $(\"#txtAddEditRecordDataCaaValue\").val(\"\");\n            $(\"#divAddEditRecordDataCaa\").show();\n            break;\n\n        case \"FWD\":\n            $(\"#txtAddEditRecordTtl\").prop(\"disabled\", true);\n            $(\"#txtAddEditRecordTtl\").val(\"0\");\n            $(\"input[name=rdAddEditRecordDataForwarderProtocol]:radio\").attr(\"disabled\", false);\n            $(\"#rdAddEditRecordDataForwarderProtocolUdp\").prop(\"checked\", true);\n            $(\"#chkAddEditRecordDataForwarderThisServer\").prop(\"checked\", false);\n            $(\"#txtAddEditRecordDataForwarder\").prop(\"disabled\", false);\n            $(\"#txtAddEditRecordDataForwarder\").val(\"\");\n            $(\"#txtAddEditRecordDataForwarderPriority\").val(\"\");\n            $(\"#chkAddEditRecordDataForwarderDnssecValidation\").prop(\"checked\", $(\"#chkDnssecValidation\").prop(\"checked\"));\n            $(\"#rdAddEditRecordDataForwarderProxyTypeDefaultProxy\").prop(\"checked\", true);\n            $(\"#txtAddEditRecordDataForwarderProxyAddress\").prop(\"disabled\", true);\n            $(\"#txtAddEditRecordDataForwarderProxyPort\").prop(\"disabled\", true);\n            $(\"#txtAddEditRecordDataForwarderProxyUsername\").prop(\"disabled\", true);\n            $(\"#txtAddEditRecordDataForwarderProxyPassword\").prop(\"disabled\", true);\n            $(\"#txtAddEditRecordDataForwarderProxyAddress\").val(\"\");\n            $(\"#txtAddEditRecordDataForwarderProxyPort\").val(\"\");\n            $(\"#txtAddEditRecordDataForwarderProxyUsername\").val(\"\");\n            $(\"#txtAddEditRecordDataForwarderProxyPassword\").val(\"\");\n            $(\"#divAddEditRecordDataForwarder\").show();\n            $(\"#divAddEditRecordDataForwarderProxy\").show();\n            break;\n\n        case \"APP\":\n            $(\"#optAddEditRecordDataAppName\").val(\"\");\n            $(\"#optAddEditRecordDataClassPath\").val(\"\");\n            $(\"#txtAddEditRecordDataData\").val(\"\");\n            $(\"#divAddEditRecordDataApplication\").show();\n\n            if (addMode)\n                loadAddRecordModalAppNames();\n\n            break;\n\n        default:\n            $(\"#txtAddEditRecordDataUnknownType\").val(\"\");\n            $(\"#lblAddEditRecordDataValue\").text(\"RDATA\");\n            $(\"#txtAddEditRecordDataValue\").val(\"\");\n            $(\"#txtAddEditRecordDataValue\").attr(\"placeholder\", \"hex string\");\n\n            $(\"#divAddEditRecordData\").show();\n            $(\"#divAddEditRecordDataUnknownType\").show();\n            break;\n    }\n}\n\nfunction zoneHasSvcbAutoHint(ipv4, ipv6) {\n    if (editZoneRecords == null)\n        return true;\n\n    for (var i = 0; i < editZoneRecords.length; i++) {\n        switch (editZoneRecords[i].type) {\n            case \"SVCB\":\n            case \"HTTPS\":\n                if ((editZoneRecords[i].rData.autoIpv4Hint && ipv4) || (editZoneRecords[i].rData.autoIpv6Hint && ipv6))\n                    return true;\n\n                break;\n        }\n    }\n\n    return false;\n}\n\nfunction addRecord() {\n    var btn = $(\"#btnAddEditRecord\");\n    var divAddEditRecordAlert = $(\"#divAddEditRecordAlert\");\n\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n\n    var domain;\n    {\n        var subDomain = $(\"#txtAddEditRecordName\").val();\n        if (subDomain === \"\")\n            subDomain = \"@\";\n\n        if (subDomain === \"@\")\n            domain = zone;\n        else if (zone === \".\")\n            domain = subDomain + \".\";\n        else\n            domain = subDomain + \".\" + zone;\n    }\n\n    var type = $(\"#optAddEditRecordType\").val();\n\n    var ttl = $(\"#txtAddEditRecordTtl\").val();\n    var overwrite = $(\"#chkAddEditRecordOverwrite\").prop(\"checked\");\n    var comments = $(\"#txtAddEditRecordComments\").val();\n    var expiryTtl = $(\"#txtAddEditRecordExpiryTtl\").val();\n\n    var apiUrl = \"\";\n\n    switch (type) {\n        case \"A\":\n        case \"AAAA\":\n            var ipAddress = $(\"#txtAddEditRecordDataValue\").val();\n            if (ipAddress === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter an IP address to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            var updateSvcbHints = zoneHasSvcbAutoHint(type == \"A\", type == \"AAAA\");\n\n            apiUrl += \"&ipAddress=\" + encodeURIComponent(ipAddress) + \"&ptr=\" + $(\"#chkAddEditRecordDataPtr\").prop('checked') + \"&createPtrZone=\" + $(\"#chkAddEditRecordDataCreatePtrZone\").prop('checked') + \"&updateSvcbHints=\" + updateSvcbHints;\n            break;\n\n        case \"NS\":\n            var nameServer = $(\"#txtAddEditRecordDataNsNameServer\").val();\n            if (nameServer === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a name server to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataNsNameServer\").trigger(\"focus\");\n                return;\n            }\n\n            var glue = cleanTextList($(\"#txtAddEditRecordDataNsGlue\").val());\n\n            apiUrl += \"&nameServer=\" + encodeURIComponent(nameServer) + \"&glue=\" + encodeURIComponent(glue);\n            break;\n\n        case \"CNAME\":\n            var subDomainName = $(\"#txtAddEditRecordName\").val();\n            if ((subDomainName === \"\") || (subDomainName === \"@\")) {\n                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);\n                $(\"#txtAddEditRecordName\").trigger(\"focus\");\n                return;\n            }\n\n            var cname = $(\"#txtAddEditRecordDataValue\").val();\n            if (cname === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&cname=\" + encodeURIComponent(cname);\n            break;\n\n        case \"PTR\":\n            var ptrName = $(\"#txtAddEditRecordDataValue\").val();\n            if (ptrName === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&ptrName=\" + encodeURIComponent(ptrName);\n            break;\n\n        case \"MX\":\n            var preference = $(\"#txtAddEditRecordDataMxPreference\").val();\n            if (preference === \"\")\n                preference = 1;\n\n            var exchange = $(\"#txtAddEditRecordDataMxExchange\").val();\n            if (exchange === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a mail exchange domain name to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataMxExchange\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&preference=\" + preference + \"&exchange=\" + encodeURIComponent(exchange);\n            break;\n\n        case \"TXT\":\n            var text = $(\"#txtAddEditRecordDataTxt\").val();\n            if (text === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataTxt\").trigger(\"focus\");\n                return;\n            }\n\n            var splitText = $(\"#chkAddEditRecordDataTxtSplitText\").prop(\"checked\");\n\n            apiUrl += \"&text=\" + encodeURIComponent(text) + \"&splitText=\" + splitText;\n            break;\n\n        case \"RP\":\n            var mailbox = $(\"#txtAddEditRecordDataRpMailbox\").val();\n            if (mailbox === \"\")\n                mailbox = \".\";\n\n            var txtDomain = $(\"#txtAddEditRecordDataRpTxtDomain\").val();\n            if (txtDomain === \"\")\n                txtDomain = \".\";\n\n            apiUrl += \"&mailbox=\" + encodeURIComponent(mailbox) + \"&txtDomain=\" + encodeURIComponent(txtDomain);\n            break;\n\n        case \"SRV\":\n            if ($(\"#txtAddEditRecordName\").val() === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a name that includes service and protocol labels.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordName\").trigger(\"focus\");\n                return;\n            }\n\n            var priority = $(\"#txtAddEditRecordDataSrvPriority\").val();\n            if (priority === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable priority.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvPriority\").trigger(\"focus\");\n                return;\n            }\n\n            var weight = $(\"#txtAddEditRecordDataSrvWeight\").val();\n            if (weight === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable weight.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvWeight\").trigger(\"focus\");\n                return;\n            }\n\n            var port = $(\"#txtAddEditRecordDataSrvPort\").val();\n            if (port === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable port number.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvPort\").trigger(\"focus\");\n                return;\n            }\n\n            var target = $(\"#txtAddEditRecordDataSrvTarget\").val();\n            if (target === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value into the target field.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvTarget\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&priority=\" + priority + \"&weight=\" + weight + \"&port=\" + port + \"&target=\" + encodeURIComponent(target);\n            break;\n\n        case \"NAPTR\":\n            var order = $(\"#txtAddEditRecordDataNaptrOrder\").val();\n            if (order === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable order.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataNaptrOrder\").trigger(\"focus\");\n                return;\n            }\n\n            var preference = $(\"#txtAddEditRecordDataNaptrPreference\").val();\n            if (preference === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable preference.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataNaptrPreference\").trigger(\"focus\");\n                return;\n            }\n\n            var flags = $(\"#txtAddEditRecordDataNaptrFlags\").val();\n            var services = $(\"#txtAddEditRecordDataNaptrServices\").val();\n            var regexp = $(\"#txtAddEditRecordDataNaptrRegExp\").val();\n            var replacement = $(\"#txtAddEditRecordDataNaptrReplacement\").val();\n\n            apiUrl += \"&naptrOrder=\" + order + \"&naptrPreference=\" + preference + \"&naptrFlags=\" + encodeURIComponent(flags) + \"&naptrServices=\" + encodeURIComponent(services) + \"&naptrRegexp=\" + encodeURIComponent(regexp) + \"&naptrReplacement=\" + encodeURIComponent(replacement);\n            break;\n\n        case \"DNAME\":\n            var dname = $(\"#txtAddEditRecordDataValue\").val();\n            if (dname === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&dname=\" + encodeURIComponent(dname);\n            break;\n\n        case \"DS\":\n            var subDomainName = $(\"#txtAddEditRecordName\").val();\n            if ((subDomainName === \"\") || (subDomainName === \"@\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a name for the DS record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordName\").trigger(\"focus\");\n                return;\n            }\n\n            var keyTag = $(\"#txtAddEditRecordDataDsKeyTag\").val();\n            if (keyTag === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Key Tag value to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataDsKeyTag\").trigger(\"focus\");\n                return;\n            }\n\n            var algorithm = $(\"#optAddEditRecordDataDsAlgorithm\").val();\n            if ((algorithm === null) || (algorithm === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select an DNSSEC algorithm to add the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataDsAlgorithm\").trigger(\"focus\");\n                return;\n            }\n\n            var digestType = $(\"#optAddEditRecordDataDsDigestType\").val();\n            if ((digestType === null) || (digestType === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Digest Type to add the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataDsDigestType\").trigger(\"focus\");\n                return;\n            }\n\n            var digest = $(\"#txtAddEditRecordDataDsDigest\").val();\n            if (digest === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Digest hash in hex string format to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataDsDigest\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&keyTag=\" + keyTag + \"&algorithm=\" + algorithm + \"&digestType=\" + digestType + \"&digest=\" + encodeURIComponent(digest);\n            break;\n\n        case \"SSHFP\":\n            var sshfpAlgorithm = $(\"#optAddEditRecordDataSshfpAlgorithm\").val();\n            if ((sshfpAlgorithm === null) || (sshfpAlgorithm === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select an Algorithm to add the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataSshfpAlgorithm\").trigger(\"focus\");\n                return;\n            }\n\n            var sshfpFingerprintType = $(\"#optAddEditRecordDataSshfpFingerprintType\").val();\n            if ((sshfpFingerprintType === null) || (sshfpFingerprintType === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Fingerprint Type to add the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataSshfpFingerprintType\").trigger(\"focus\");\n                return;\n            }\n\n            var sshfpFingerprint = $(\"#txtAddEditRecordDataSshfpFingerprint\").val();\n            if (sshfpFingerprint === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Fingerprint hash in hex string format to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSshfpFingerprint\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&sshfpAlgorithm=\" + sshfpAlgorithm + \"&sshfpFingerprintType=\" + sshfpFingerprintType + \"&sshfpFingerprint=\" + encodeURIComponent(sshfpFingerprint);\n            break;\n\n        case \"TLSA\":\n            var tlsaCertificateUsage = $(\"#optAddEditRecordDataTlsaCertificateUsage\").val();\n            if ((tlsaCertificateUsage === null) || (tlsaCertificateUsage === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Certificate Usage to add the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataTlsaCertificateUsage\").trigger(\"focus\");\n                return;\n            }\n\n            var tlsaSelector = $(\"#optAddEditRecordDataTlsaSelector\").val();\n            if ((tlsaSelector === null) || (tlsaSelector === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Selector to add the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataTlsaSelector\").trigger(\"focus\");\n                return;\n            }\n\n            var tlsaMatchingType = $(\"#optAddEditRecordDataTlsaMatchingType\").val();\n            if ((tlsaMatchingType === null) || (tlsaMatchingType === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Matching Type to add the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataTlsaMatchingType\").trigger(\"focus\");\n                return;\n            }\n\n            var tlsaCertificateAssociationData = $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").val();\n            if (tlsaCertificateAssociationData === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Certificate Association Data to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").trigger(\"focus\");\n                return;\n            }\n\n            if ((tlsaMatchingType === \"Full\") && !tlsaCertificateAssociationData.startsWith(\"-\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a complete certificate in PEM format as the Certificate Association Data to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&tlsaCertificateUsage=\" + tlsaCertificateUsage + \"&tlsaSelector=\" + tlsaSelector + \"&tlsaMatchingType=\" + tlsaMatchingType + \"&tlsaCertificateAssociationData=\" + encodeURIComponent(tlsaCertificateAssociationData);\n            break;\n\n        case \"SVCB\":\n        case \"HTTPS\":\n            var svcPriority = $(\"#txtAddEditRecordDataSvcbPriority\").val();\n            if ((svcPriority === null) || (svcPriority === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a Priority value to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSvcbPriority\").trigger(\"focus\");\n                return;\n            }\n\n            var svcTargetName = $(\"#txtAddEditRecordDataSvcbTargetName\").val();\n            if ((svcTargetName === null) || (svcTargetName === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a Target Name to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSvcbTargetName\").trigger(\"focus\");\n                return;\n            }\n\n            var svcParams = serializeTableData($(\"#tableAddEditRecordDataSvcbParams\"), 2, divAddEditRecordAlert);\n            if (svcParams === false)\n                return;\n\n            if (svcParams.length === 0)\n                svcParams = false;\n\n            var autoIpv4Hint = $(\"#chkAddEditRecordDataSvcbAutoIpv4Hint\").prop(\"checked\");\n            var autoIpv6Hint = $(\"#chkAddEditRecordDataSvcbAutoIpv6Hint\").prop(\"checked\");\n\n            apiUrl += \"&svcPriority=\" + svcPriority + \"&svcTargetName=\" + encodeURIComponent(svcTargetName) + \"&svcParams=\" + encodeURIComponent(svcParams) + \"&autoIpv4Hint=\" + autoIpv4Hint + \"&autoIpv6Hint=\" + autoIpv6Hint;\n            break;\n\n        case \"URI\":\n            var uriPriority = $(\"#txtAddEditRecordDataUriPriority\").val();\n            if (uriPriority === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable priority.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataUriPriority\").trigger(\"focus\");\n                return;\n            }\n\n            var uriWeight = $(\"#txtAddEditRecordDataUriWeight\").val();\n            if (uriWeight === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable weight.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataUriWeight\").trigger(\"focus\");\n                return;\n            }\n\n            var uri = $(\"#txtAddEditRecordDataUri\").val();\n            if (uri === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value into the URI field.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataUri\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&uriPriority=\" + uriPriority + \"&uriWeight=\" + uriWeight + \"&uri=\" + encodeURIComponent(uri);\n            break;\n\n        case \"CAA\":\n            var flags = $(\"#txtAddEditRecordDataCaaFlags\").val();\n            if (flags === \"\")\n                flags = 0;\n\n            var tag = $(\"#txtAddEditRecordDataCaaTag\").val();\n            if (tag === \"\")\n                tag = \"issue\";\n\n            var value = $(\"#txtAddEditRecordDataCaaValue\").val();\n            if (value === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value into the authority field.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataCaaValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&flags=\" + flags + \"&tag=\" + encodeURIComponent(tag) + \"&value=\" + encodeURIComponent(value);\n            break;\n\n        case \"ANAME\":\n            var aname = $(\"#txtAddEditRecordDataValue\").val();\n            if (aname === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&aname=\" + encodeURIComponent(aname);\n            break;\n\n        case \"FWD\":\n            var forwarder = $(\"#txtAddEditRecordDataForwarder\").val();\n            if (forwarder === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a domain name or IP address or URL as a forwarder to add the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataForwarder\").trigger(\"focus\");\n                return;\n            }\n\n            var forwarderPriority = $(\"#txtAddEditRecordDataForwarderPriority\").val();\n            var dnssecValidation = $(\"#chkAddEditRecordDataForwarderDnssecValidation\").prop(\"checked\");\n            var proxyType = $(\"input[name=rdAddEditRecordDataForwarderProxyType]:checked\").val();\n\n            apiUrl += \"&protocol=\" + $('input[name=rdAddEditRecordDataForwarderProtocol]:checked').val() + \"&forwarder=\" + encodeURIComponent(forwarder);\n            apiUrl += \"&forwarderPriority=\" + forwarderPriority + \"&dnssecValidation=\" + dnssecValidation + \"&proxyType=\" + proxyType;\n\n            switch (proxyType) {\n                case \"Http\":\n                case \"Socks5\":\n                    var proxyAddress = $(\"#txtAddEditRecordDataForwarderProxyAddress\").val();\n                    var proxyPort = $(\"#txtAddEditRecordDataForwarderProxyPort\").val();\n                    var proxyUsername = $(\"#txtAddEditRecordDataForwarderProxyUsername\").val();\n                    var proxyPassword = $(\"#txtAddEditRecordDataForwarderProxyPassword\").val();\n\n                    if ((proxyAddress == null) || (proxyAddress === \"\")) {\n                        showAlert(\"warning\", \"Missing!\", \"Please enter a domain name or IP address for Proxy Server Address to add the record.\", divAddEditRecordAlert);\n                        $(\"#txtAddEditRecordDataForwarderProxyAddress\").trigger(\"focus\");\n                        return;\n                    }\n\n                    if ((proxyPort == null) || (proxyPort === \"\")) {\n                        showAlert(\"warning\", \"Missing!\", \"Please enter a port number for Proxy Server Port to add the record.\", divAddEditRecordAlert);\n                        $(\"#txtAddEditRecordDataForwarderProxyPort\").trigger(\"focus\");\n                        return;\n                    }\n\n                    apiUrl += \"&proxyAddress=\" + encodeURIComponent(proxyAddress) + \"&proxyPort=\" + proxyPort + \"&proxyUsername=\" + encodeURIComponent(proxyUsername) + \"&proxyPassword=\" + encodeURIComponent(proxyPassword);\n                    break;\n            }\n            break;\n\n        case \"APP\":\n            var appName = $(\"#optAddEditRecordDataAppName\").val();\n\n            if ((appName === null) || (appName === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select an application name to add record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataAppName\").trigger(\"focus\");\n                return;\n            }\n\n            var classPath = $(\"#optAddEditRecordDataClassPath\").val();\n\n            if ((classPath === null) || (classPath === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a class path to add record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataClassPath\").trigger(\"focus\");\n                return;\n            }\n\n            var recordData = $(\"#txtAddEditRecordDataData\").val();\n\n            apiUrl += \"&appName=\" + encodeURIComponent(appName) + \"&classPath=\" + encodeURIComponent(classPath) + \"&recordData=\" + encodeURIComponent(recordData);\n            break;\n\n        default:\n            type = $(\"#txtAddEditRecordDataUnknownType\").val();\n            if ((type === null) || (type === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a resoure record name or number to add record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataUnknownType\").trigger(\"focus\");\n                return;\n            }\n\n            var rdata = $(\"#txtAddEditRecordDataValue\").val();\n            if ((rdata === null) || (rdata === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a hex value as the RDATA to add record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&rdata=\" + encodeURIComponent(rdata);\n            break;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    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;\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#modalAddEditRecord\").modal(\"hide\");\n\n            if (overwrite) {\n                var currentPageNumber = Number($(\"#txtEditZonePageNumber\").val());\n                showEditZone(zone, currentPageNumber);\n            }\n            else {\n                //update local array\n                editZoneRecords.unshift(responseJSON.response.addedRecord);\n                editZoneFilteredRecords = null; //to evaluate filters again\n\n                //show page\n                showEditZonePage(1);\n            }\n\n            showAlert(\"success\", \"Record Added!\", \"Resource record was added successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalAddEditRecord\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAddEditRecordAlert\n    });\n}\n\nfunction updateAddEditFormForwarderPlaceholder() {\n    var protocol = $('input[name=rdAddEditRecordDataForwarderProtocol]:checked').val();\n    switch (protocol) {\n        case \"Udp\":\n        case \"Tcp\":\n            $(\"#txtAddEditRecordDataForwarder\").attr(\"placeholder\", \"8.8.8.8 or [2620:fe::10]\")\n            break;\n\n        case \"Tls\":\n        case \"Quic\":\n            $(\"#txtAddEditRecordDataForwarder\").attr(\"placeholder\", \"dns.quad9.net (9.9.9.9:853)\")\n            break;\n\n        case \"Https\":\n            $(\"#txtAddEditRecordDataForwarder\").attr(\"placeholder\", \"https://cloudflare-dns.com/dns-query (1.1.1.1)\")\n            break;\n    }\n}\n\nfunction updateAddEditFormForwarderProxyType() {\n    var proxyType = $('input[name=rdAddEditRecordDataForwarderProxyType]:checked').val();\n    var disabled = (proxyType === \"NoProxy\") || (proxyType === \"DefaultProxy\");\n\n    $(\"#txtAddEditRecordDataForwarderProxyAddress\").prop(\"disabled\", disabled);\n    $(\"#txtAddEditRecordDataForwarderProxyPort\").prop(\"disabled\", disabled);\n    $(\"#txtAddEditRecordDataForwarderProxyUsername\").prop(\"disabled\", disabled);\n    $(\"#txtAddEditRecordDataForwarderProxyPassword\").prop(\"disabled\", disabled);\n}\n\nfunction updateAddEditFormForwarderThisServer() {\n    var useThisServer = $(\"#chkAddEditRecordDataForwarderThisServer\").prop('checked');\n\n    if (useThisServer) {\n        $(\"input[name=rdAddEditRecordDataForwarderProtocol]:radio\").attr(\"disabled\", true);\n        $(\"#rdAddEditRecordDataForwarderProtocolUdp\").prop(\"checked\", true);\n        $(\"#txtAddEditRecordDataForwarder\").attr(\"placeholder\", \"8.8.8.8 or [2620:fe::10]\")\n\n        $(\"#txtAddEditRecordDataForwarder\").prop(\"disabled\", true);\n        $(\"#txtAddEditRecordDataForwarder\").val(\"this-server\");\n\n        $(\"#divAddEditRecordDataForwarderProxy\").hide();\n    }\n    else {\n        $(\"input[name=rdAddEditRecordDataForwarderProtocol]:radio\").attr(\"disabled\", false);\n\n        $(\"#txtAddEditRecordDataForwarder\").prop(\"disabled\", false);\n        $(\"#txtAddEditRecordDataForwarder\").val(\"\");\n\n        $(\"#divAddEditRecordDataForwarderProxy\").show();\n    }\n}\n\nfunction addSvcbRecordParamEditRow(paramKey, paramValue) {\n    var id = Math.floor(Math.random() * 10000);\n\n    var tableHtmlRows = \"<tr id=\\\"tableAddEditRecordDataSvcbParamsRow\" + id + \"\\\">\";\n\n    if ((paramKey != \"\") && isFinite(paramKey)) {\n        tableHtmlRows += \"<td><input type=\\\"text\\\" class=\\\"form-control\\\" placeholder=\\\"key number\\\" value=\\\"\" + htmlEncode(paramKey) + \"\\\"></td>\";\n        tableHtmlRows += \"<td><input type=\\\"text\\\" data-optional=\\\"true\\\" class=\\\"form-control\\\" placeholder=\\\"hex string\\\" value=\\\"\" + htmlEncode(paramValue) + \"\\\"></td>\";\n    }\n    else {\n        tableHtmlRows += \"<td id=\\\"tableAddEditRecordDataSvcbParamsRowColumn1\" + id + \"\\\">\";\n        tableHtmlRows += \"<select class=\\\"form-control\\\" onchange=\\\"if (event.target.value === 'Unknown') { $('#tableAddEditRecordDataSvcbParamsRowColumn1\" + id + \"').html('<input type=\\\\\\'text\\\\\\' class=\\\\\\'form-control\\\\\\' placeholder=\\\\\\'key number\\\\\\' >'); $('#tableAddEditRecordDataSvcbParamsRowColumn2\" + id + \"').html('<input type=\\\\\\'text\\\\\\' data-optional=\\\\\\'true\\\\\\' class=\\\\\\'form-control\\\\\\' placeholder=\\\\\\'hex string\\\\\\' >'); }\\\">\";\n        tableHtmlRows += \"<option\" + (paramKey == \"mandatory\" ? \" selected\" : \"\") + \">mandatory</option>\";\n        tableHtmlRows += \"<option\" + (paramKey == \"alpn\" ? \" selected\" : \"\") + \">alpn</option>\";\n        tableHtmlRows += \"<option\" + (paramKey == \"no-default-alpn\" ? \" selected\" : \"\") + \">no-default-alpn</option>\";\n        tableHtmlRows += \"<option\" + (paramKey == \"port\" ? \" selected\" : \"\") + \">port</option>\";\n        tableHtmlRows += \"<option\" + (paramKey == \"ipv4hint\" ? \" selected\" : \"\") + \">ipv4hint</option>\";\n        tableHtmlRows += \"<option\" + (paramKey == \"ipv6hint\" ? \" selected\" : \"\") + \">ipv6hint</option>\";\n        tableHtmlRows += \"<option\" + (paramKey == \"dohpath\" ? \" selected\" : \"\") + \">dohpath</option>\";\n        tableHtmlRows += \"<option>Unknown</option>\";\n        tableHtmlRows += \"</select></td>\";\n\n        tableHtmlRows += \"<td id=\\\"tableAddEditRecordDataSvcbParamsRowColumn2\" + id + \"\\\"><input type=\\\"text\\\" data-optional=\\\"true\\\" class=\\\"form-control\\\" value=\\\"\" + htmlEncode(paramValue) + \"\\\"></td>\";\n    }\n\n    tableHtmlRows += \"<td><button type=\\\"button\\\" class=\\\"btn btn-warning\\\" onclick=\\\"$('#tableAddEditRecordDataSvcbParamsRow\" + id + \"').remove();\\\">Remove</button></td></tr>\";\n\n    $(\"#tableAddEditRecordDataSvcbParams\").append(tableHtmlRows);\n}\n\nfunction showEditRecordModal(objBtn) {\n    var btn = $(objBtn);\n    var id = btn.attr(\"data-id\");\n    var divData = $(\"#data\" + id);\n\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n    var zoneType = $(\"#titleEditZone\").attr(\"data-zone-type\");\n    var catalogZone = $(\"#titleEditZoneCatalog\").text();\n    var name = divData.attr(\"data-record-name\");\n    var type = divData.attr(\"data-record-type\");\n    var ttl = divData.attr(\"data-record-ttl\");\n    var comments = divData.attr(\"data-record-comments\");\n    var expiryTtl = divData.attr(\"data-record-expiry-ttl\");\n\n    if (name === zone)\n        name = \"@\";\n    else\n        name = name.replace(\".\" + zone, \"\");\n\n    clearAddEditRecordForm();\n    $(\"#titleAddEditRecord\").text(\"Edit Record\");\n    $(\"#lblAddEditRecordZoneName\").text(zone === \".\" ? \"\" : zone);\n    $(\"#optEditRecordTypeSoa\").show();\n    $(\"#optAddEditRecordType\").val(type);\n    $(\"#divAddEditRecordOverwrite\").hide();\n    modifyAddRecordFormByType(false);\n\n    $(\"#txtAddEditRecordName\").val(name);\n    $(\"#txtAddEditRecordTtl\").val(ttl)\n    $(\"#txtAddEditRecordComments\").val(comments);\n    $(\"#txtAddEditRecordExpiryTtl\").val(expiryTtl);\n\n    switch (type) {\n        case \"A\":\n        case \"AAAA\":\n            $(\"#txtAddEditRecordDataValue\").val(divData.attr(\"data-record-ip-address\"));\n            $(\"#chkAddEditRecordDataPtr\").prop(\"checked\", false);\n            $(\"#chkAddEditRecordDataCreatePtrZone\").prop(\"disabled\", true);\n            $(\"#chkAddEditRecordDataCreatePtrZone\").prop(\"checked\", false);\n            $(\"#chkAddEditRecordDataPtrLabel\").text(\"Update reverse (PTR) record\");\n            break;\n\n        case \"NS\":\n            if ((zoneType == \"Primary\") && (name == \"@\") && sessionData.info.clusterInitialized && (catalogZone == \"cluster-catalog.\" + sessionData.info.clusterDomain)) {\n                $(\"#txtAddEditRecordName\").prop(\"disabled\", true);\n                $(\"#txtAddEditRecordDataNsNameServer\").prop(\"disabled\", true);\n                $(\"#txtAddEditRecordDataNsGlue\").prop(\"disabled\", true);\n                $(\"#txtAddEditRecordExpiryTtl\").prop(\"disabled\", true);\n            }\n\n            $(\"#txtAddEditRecordDataNsNameServer\").val(divData.attr(\"data-record-name-server\"));\n            $(\"#txtAddEditRecordDataNsGlue\").val(divData.attr(\"data-record-glue\").replace(/, /g, \"\\n\"));\n            break;\n\n        case \"CNAME\":\n            $(\"#txtAddEditRecordDataValue\").val(divData.attr(\"data-record-cname\"));\n            break;\n\n        case \"SOA\":\n            $(\"#txtEditRecordDataSoaPrimaryNameServer\").val(divData.attr(\"data-record-pname\"));\n            $(\"#txtEditRecordDataSoaResponsiblePerson\").val(divData.attr(\"data-record-rperson\"));\n            $(\"#txtEditRecordDataSoaSerial\").val(divData.attr(\"data-record-serial\"));\n            $(\"#txtEditRecordDataSoaSerial\").prop(\"disabled\", divData.attr(\"data-record-serial-scheme\") === \"true\");\n            $(\"#txtEditRecordDataSoaRefresh\").val(divData.attr(\"data-record-refresh\"));\n            $(\"#txtEditRecordDataSoaRetry\").val(divData.attr(\"data-record-retry\"));\n            $(\"#txtEditRecordDataSoaExpire\").val(divData.attr(\"data-record-expire\"));\n            $(\"#txtEditRecordDataSoaMinimum\").val(divData.attr(\"data-record-minimum\"));\n            $(\"#chkEditRecordDataSoaUseSerialDateScheme\").prop(\"checked\", divData.attr(\"data-record-serial-scheme\") === \"true\");\n\n            $(\"#txtAddEditRecordName\").prop(\"disabled\", true);\n            $(\"#divAddEditRecordExpiryTtl\").hide();\n\n            switch (zoneType) {\n                case \"Primary\":\n                    if (sessionData.info.clusterInitialized && (catalogZone == \"cluster-catalog.\" + sessionData.info.clusterDomain))\n                        $(\"#txtEditRecordDataSoaPrimaryNameServer\").prop(\"disabled\", true);\n                    else\n                        $(\"#txtEditRecordDataSoaPrimaryNameServer\").prop(\"disabled\", false);\n\n                    $(\"#txtAddEditRecordTtl\").prop(\"disabled\", false);\n                    $(\"#txtEditRecordDataSoaResponsiblePerson\").prop(\"disabled\", false);\n                    break;\n\n                case \"Forwarder\":\n                    $(\"#txtAddEditRecordTtl\").prop(\"disabled\", true);\n                    $(\"#txtEditRecordDataSoaResponsiblePerson\").prop(\"disabled\", true);\n                    break;\n\n                case \"Catalog\":\n                    $(\"#txtAddEditRecordTtl\").prop(\"disabled\", true);\n                    $(\"#txtEditRecordDataSoaPrimaryNameServer\").prop(\"disabled\", true);\n                    $(\"#txtEditRecordDataSoaResponsiblePerson\").prop(\"disabled\", true);\n                    break;\n\n                default:\n                    $(\"#txtAddEditRecordTtl\").prop(\"disabled\", false);\n                    $(\"#txtEditRecordDataSoaPrimaryNameServer\").prop(\"disabled\", false);\n                    $(\"#txtEditRecordDataSoaResponsiblePerson\").prop(\"disabled\", false);\n                    break;\n            }\n\n            break;\n\n        case \"PTR\":\n            $(\"#txtAddEditRecordDataValue\").val(divData.attr(\"data-record-ptr-name\"));\n            break;\n\n        case \"MX\":\n            $(\"#txtAddEditRecordDataMxPreference\").val(divData.attr(\"data-record-preference\"));\n            $(\"#txtAddEditRecordDataMxExchange\").val(divData.attr(\"data-record-exchange\"));\n            break;\n\n        case \"TXT\":\n            $(\"#txtAddEditRecordDataTxt\").val(divData.attr(\"data-record-text\"));\n            $(\"#chkAddEditRecordDataTxtSplitText\").prop(\"checked\", divData.attr(\"data-record-split-text\") === \"true\");\n            break;\n\n        case \"RP\":\n            $(\"#txtAddEditRecordDataRpMailbox\").val(divData.attr(\"data-record-mailbox\"));\n            $(\"#txtAddEditRecordDataRpTxtDomain\").val(divData.attr(\"data-record-txt-domain\"));\n            break;\n\n        case \"SRV\":\n            $(\"#txtAddEditRecordDataSrvPriority\").val(divData.attr(\"data-record-priority\"));\n            $(\"#txtAddEditRecordDataSrvWeight\").val(divData.attr(\"data-record-weight\"));\n            $(\"#txtAddEditRecordDataSrvPort\").val(divData.attr(\"data-record-port\"));\n            $(\"#txtAddEditRecordDataSrvTarget\").val(divData.attr(\"data-record-target\"));\n            break;\n\n        case \"NAPTR\":\n            $(\"#txtAddEditRecordDataNaptrOrder\").val(divData.attr(\"data-record-order\"));\n            $(\"#txtAddEditRecordDataNaptrPreference\").val(divData.attr(\"data-record-preference\"));\n            $(\"#txtAddEditRecordDataNaptrFlags\").val(divData.attr(\"data-record-flags\"));\n            $(\"#txtAddEditRecordDataNaptrServices\").val(divData.attr(\"data-record-services\"));\n            $(\"#txtAddEditRecordDataNaptrRegExp\").val(divData.attr(\"data-record-regexp\"));\n            $(\"#txtAddEditRecordDataNaptrReplacement\").val(divData.attr(\"data-record-replacement\"));\n            break;\n\n        case \"DNAME\":\n            $(\"#txtAddEditRecordDataValue\").val(divData.attr(\"data-record-dname\"));\n            break;\n\n        case \"DS\":\n            $(\"#txtAddEditRecordDataDsKeyTag\").val(divData.attr(\"data-record-key-tag\"));\n            $(\"#optAddEditRecordDataDsAlgorithm\").val(divData.attr(\"data-record-algorithm\"));\n            $(\"#optAddEditRecordDataDsDigestType\").val(divData.attr(\"data-record-digest-type\"));\n            $(\"#txtAddEditRecordDataDsDigest\").val(divData.attr(\"data-record-digest\"));\n            break;\n\n        case \"SSHFP\":\n            $(\"#optAddEditRecordDataSshfpAlgorithm\").val(divData.attr(\"data-record-algorithm\"));\n            $(\"#optAddEditRecordDataSshfpFingerprintType\").val(divData.attr(\"data-record-fingerprint-type\"));\n            $(\"#txtAddEditRecordDataSshfpFingerprint\").val(divData.attr(\"data-record-fingerprint\"));\n            break;\n\n        case \"TLSA\":\n            $(\"#optAddEditRecordDataTlsaCertificateUsage\").val(divData.attr(\"data-record-certificate-usage\"));\n            $(\"#optAddEditRecordDataTlsaSelector\").val(divData.attr(\"data-record-selector\"));\n            $(\"#optAddEditRecordDataTlsaMatchingType\").val(divData.attr(\"data-record-matching-type\"));\n            $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").val(divData.attr(\"data-record-certificate-association-data\"));\n            break;\n\n        case \"SVCB\":\n        case \"HTTPS\":\n            $(\"#txtAddEditRecordDataSvcbPriority\").val(divData.attr(\"data-record-svc-priority\"));\n            $(\"#txtAddEditRecordDataSvcbTargetName\").val(divData.attr(\"data-record-svc-target-name\"));\n\n            var svcParams = JSON.parse(divData.attr(\"data-record-svc-params\"));\n            var autoIpv4Hint = divData.attr(\"data-record-auto-ipv4hint\") === \"true\";\n            var autoIpv6Hint = divData.attr(\"data-record-auto-ipv6hint\") === \"true\";\n\n            for (var paramKey in svcParams) {\n                switch (paramKey) {\n                    case \"ipv4hint\":\n                        if (autoIpv4Hint)\n                            continue;\n\n                        break;\n\n                    case \"ipv6hint\":\n                        if (autoIpv6Hint)\n                            continue;\n\n                        break;\n                }\n\n                addSvcbRecordParamEditRow(paramKey, svcParams[paramKey]);\n            }\n\n            $(\"#chkAddEditRecordDataSvcbAutoIpv4Hint\").prop(\"checked\", autoIpv4Hint);\n            $(\"#chkAddEditRecordDataSvcbAutoIpv6Hint\").prop(\"checked\", autoIpv6Hint);\n            break;\n\n        case \"URI\":\n            $(\"#txtAddEditRecordDataUriPriority\").val(divData.attr(\"data-record-priority\"));\n            $(\"#txtAddEditRecordDataUriWeight\").val(divData.attr(\"data-record-weight\"));\n            $(\"#txtAddEditRecordDataUri\").val(divData.attr(\"data-record-uri\"));\n            break;\n\n        case \"CAA\":\n            $(\"#txtAddEditRecordDataCaaFlags\").val(divData.attr(\"data-record-flags\"));\n            $(\"#txtAddEditRecordDataCaaTag\").val(divData.attr(\"data-record-tag\"));\n            $(\"#txtAddEditRecordDataCaaValue\").val(divData.attr(\"data-record-value\"));\n            break;\n\n        case \"ANAME\":\n            $(\"#txtAddEditRecordDataValue\").val(divData.attr(\"data-record-aname\"));\n            break;\n\n        case \"FWD\":\n            $(\"#txtAddEditRecordTtl\").prop(\"disabled\", true);\n            $(\"#rdAddEditRecordDataForwarderProtocol\" + divData.attr(\"data-record-protocol\")).prop(\"checked\", true);\n\n            var forwarder = divData.attr(\"data-record-forwarder\");\n\n            $(\"#chkAddEditRecordDataForwarderThisServer\").prop(\"checked\", (forwarder == \"this-server\"));\n            $(\"#txtAddEditRecordDataForwarder\").prop(\"disabled\", (forwarder == \"this-server\"));\n            $(\"#txtAddEditRecordDataForwarder\").val(forwarder);\n\n            if (forwarder === \"this-server\") {\n                $(\"input[name=rdAddEditRecordDataForwarderProtocol]:radio\").attr(\"disabled\", true);\n                $(\"#divAddEditRecordDataForwarderProxy\").hide();\n            }\n            else {\n                $(\"input[name=rdAddEditRecordDataForwarderProtocol]:radio\").attr(\"disabled\", false);\n                $(\"#divAddEditRecordDataForwarderProxy\").show();\n            }\n\n            $(\"#txtAddEditRecordDataForwarderPriority\").val(divData.attr(\"data-record-priority\"));\n            $(\"#chkAddEditRecordDataForwarderDnssecValidation\").prop(\"checked\", divData.attr(\"data-record-dnssec-validation\") === \"true\");\n\n            var proxyType = divData.attr(\"data-record-proxy-type\");\n            $(\"#rdAddEditRecordDataForwarderProxyType\" + proxyType).prop(\"checked\", true);\n\n            switch (proxyType) {\n                case \"Http\":\n                case \"Socks5\":\n                    $(\"#txtAddEditRecordDataForwarderProxyAddress\").val(divData.attr(\"data-record-proxy-address\"));\n                    $(\"#txtAddEditRecordDataForwarderProxyPort\").val(divData.attr(\"data-record-proxy-port\"));\n                    $(\"#txtAddEditRecordDataForwarderProxyUsername\").val(divData.attr(\"data-record-proxy-username\"));\n                    $(\"#txtAddEditRecordDataForwarderProxyPassword\").val(divData.attr(\"data-record-proxy-password\"));\n                    break;\n            }\n\n            updateAddEditFormForwarderPlaceholder();\n            updateAddEditFormForwarderProxyType();\n            break;\n\n        case \"APP\":\n            $(\"#optAddEditRecordDataAppName\").prop(\"disabled\", true);\n            $(\"#optAddEditRecordDataClassPath\").prop(\"disabled\", true);\n\n            $(\"#optAddEditRecordDataAppName\").html(\"<option>\" + divData.attr(\"data-record-app-name\") + \"</option>\")\n            $(\"#optAddEditRecordDataAppName\").val(divData.attr(\"data-record-app-name\"))\n\n            $(\"#optAddEditRecordDataClassPath\").html(\"<option>\" + divData.attr(\"data-record-classpath\") + \"</option>\")\n            $(\"#optAddEditRecordDataClassPath\").val(divData.attr(\"data-record-classpath\"))\n\n            $(\"#txtAddEditRecordDataData\").val(divData.attr(\"data-record-data\"))\n            break;\n\n        default:\n            var rdata = divData.attr(\"data-record-rdata\");\n\n            if (rdata == null) {\n                showAlert(\"danger\", \"Not Supported!\", \"Editing this record type is not supported.\");\n                return;\n            }\n\n            $(\"#optAddEditRecordType\").val(\"Unknown\");\n            $(\"#txtAddEditRecordDataUnknownType\").val(type);\n            $(\"#txtAddEditRecordDataUnknownType\").prop(\"disabled\", true);\n\n            $(\"#txtAddEditRecordDataValue\").val(rdata);\n            break;\n    }\n\n    $(\"#optAddEditRecordType\").prop(\"disabled\", true);\n\n    $(\"#btnAddEditRecord\").attr(\"data-id\", id);\n    $(\"#btnAddEditRecord\").attr(\"onclick\", \"updateRecord(); return false;\");\n\n    $(\"#modalAddEditRecord\").modal(\"show\");\n\n    setTimeout(function () {\n        $(\"#txtAddEditRecordName\").trigger(\"focus\");\n    }, 1000);\n}\n\nfunction updateRecord() {\n    var btn = $(\"#btnAddEditRecord\");\n    var divAddEditRecordAlert = $(\"#divAddEditRecordAlert\");\n\n    var index = Number(btn.attr(\"data-id\"));\n    var divData = $(\"#data\" + index);\n\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n    var recordIndex = Number(divData.attr(\"data-record-index\"));\n    var type = divData.attr(\"data-record-type\");\n    var domain = divData.attr(\"data-record-name\");\n\n    if (domain === \"\")\n        domain = \".\";\n\n    var newDomain;\n    {\n        var newSubDomain = $(\"#txtAddEditRecordName\").val();\n        if (newSubDomain === \"\")\n            newSubDomain = \"@\";\n\n        if (newSubDomain === \"@\")\n            newDomain = zone;\n        else if (zone === \".\")\n            newDomain = newSubDomain + \".\";\n        else\n            newDomain = newSubDomain + \".\" + zone;\n    }\n\n    var ttl = $(\"#txtAddEditRecordTtl\").val();\n    var disable = (divData.attr(\"data-record-disabled\") === \"true\");\n    var comments = $(\"#txtAddEditRecordComments\").val();\n    var expiryTtl = $(\"#txtAddEditRecordExpiryTtl\").val();\n\n    var apiUrl = \"\";\n\n    switch (type) {\n        case \"A\":\n        case \"AAAA\":\n            var ipAddress = divData.attr(\"data-record-ip-address\");\n\n            var newIpAddress = $(\"#txtAddEditRecordDataValue\").val();\n            if (newIpAddress === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter an IP address to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            var updateSvcbHints = zoneHasSvcbAutoHint(type == \"A\", type == \"AAAA\");\n\n            apiUrl += \"&ipAddress=\" + encodeURIComponent(ipAddress) + \"&newIpAddress=\" + encodeURIComponent(newIpAddress) + \"&ptr=\" + $(\"#chkAddEditRecordDataPtr\").prop('checked') + \"&createPtrZone=\" + $(\"#chkAddEditRecordDataCreatePtrZone\").prop('checked') + \"&updateSvcbHints=\" + updateSvcbHints;\n            break;\n\n        case \"NS\":\n            var nameServer = divData.attr(\"data-record-name-server\");\n\n            var newNameServer = $(\"#txtAddEditRecordDataNsNameServer\").val();\n            if (newNameServer === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a name server to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataNsNameServer\").trigger(\"focus\");\n                return;\n            }\n\n            var glue = cleanTextList($(\"#txtAddEditRecordDataNsGlue\").val());\n\n            apiUrl += \"&nameServer=\" + encodeURIComponent(nameServer) + \"&newNameServer=\" + encodeURIComponent(newNameServer) + \"&glue=\" + encodeURIComponent(glue);\n            break;\n\n        case \"CNAME\":\n            var subDomainName = $(\"#txtAddEditRecordName\").val();\n            if ((subDomainName === \"\") || (subDomainName === \"@\")) {\n                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);\n                $(\"#txtAddEditRecordName\").trigger(\"focus\");\n                return;\n            }\n\n            var cname = $(\"#txtAddEditRecordDataValue\").val();\n            if (cname === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&cname=\" + encodeURIComponent(cname);\n            break;\n\n        case \"SOA\":\n            var primaryNameServer = $(\"#txtEditRecordDataSoaPrimaryNameServer\").val();\n            if (primaryNameServer === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a value for primary name server.\", divAddEditRecordAlert);\n                $(\"#txtEditRecordDataSoaPrimaryNameServer\").trigger(\"focus\");\n                return;\n            }\n\n            var responsiblePerson = $(\"#txtEditRecordDataSoaResponsiblePerson\").val();\n            if (responsiblePerson === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a value for responsible person.\", divAddEditRecordAlert);\n                $(\"#txtEditRecordDataSoaResponsiblePerson\").trigger(\"focus\");\n                return;\n            }\n\n            var serial = $(\"#txtEditRecordDataSoaSerial\").val();\n            if (serial === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a value for serial.\", divAddEditRecordAlert);\n                $(\"#txtEditRecordDataSoaSerial\").trigger(\"focus\");\n                return;\n            }\n\n            var refresh = $(\"#txtEditRecordDataSoaRefresh\").val();\n            if (refresh === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a value for refresh.\", divAddEditRecordAlert);\n                $(\"#txtEditRecordDataSoaRefresh\").trigger(\"focus\");\n                return;\n            }\n\n            var retry = $(\"#txtEditRecordDataSoaRetry\").val();\n            if (retry === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a value for retry.\", divAddEditRecordAlert);\n                $(\"#txtEditRecordDataSoaRetry\").trigger(\"focus\");\n                return;\n            }\n\n            var expire = $(\"#txtEditRecordDataSoaExpire\").val();\n            if (expire === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a value for expire.\", divAddEditRecordAlert);\n                $(\"#txtEditRecordDataSoaExpire\").trigger(\"focus\");\n                return;\n            }\n\n            var minimum = $(\"#txtEditRecordDataSoaMinimum\").val();\n            if (minimum === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a value for minimum.\", divAddEditRecordAlert);\n                $(\"#txtEditRecordDataSoaMinimum\").trigger(\"focus\");\n                return;\n            }\n\n            var useSerialDateScheme = $(\"#chkEditRecordDataSoaUseSerialDateScheme\").prop(\"checked\");\n\n            apiUrl += \"&primaryNameServer=\" + encodeURIComponent(primaryNameServer) +\n                \"&responsiblePerson=\" + encodeURIComponent(responsiblePerson) +\n                \"&serial=\" + encodeURIComponent(serial) +\n                \"&refresh=\" + encodeURIComponent(refresh) +\n                \"&retry=\" + encodeURIComponent(retry) +\n                \"&expire=\" + encodeURIComponent(expire) +\n                \"&minimum=\" + encodeURIComponent(minimum) +\n                \"&useSerialDateScheme=\" + encodeURIComponent(useSerialDateScheme);\n\n            break;\n\n        case \"PTR\":\n            var ptrName = divData.attr(\"data-record-ptr-name\");\n\n            var newPtrName = $(\"#txtAddEditRecordDataValue\").val();\n            if (newPtrName === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&ptrName=\" + encodeURIComponent(ptrName) + \"&newPtrName=\" + encodeURIComponent(newPtrName);\n            break;\n\n        case \"MX\":\n            var preference = divData.attr(\"data-record-preference\");\n\n            var newPreference = $(\"#txtAddEditRecordDataMxPreference\").val();\n            if (newPreference === \"\")\n                newPreference = 1;\n\n            var exchange = divData.attr(\"data-record-exchange\");\n\n            var newExchange = $(\"#txtAddEditRecordDataMxExchange\").val();\n            if (newExchange === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a mail exchange domain name to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataMxExchange\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&preference=\" + preference + \"&newPreference=\" + newPreference + \"&exchange=\" + encodeURIComponent(exchange) + \"&newExchange=\" + encodeURIComponent(newExchange);\n            break;\n\n        case \"TXT\":\n            var text = divData.attr(\"data-record-text\");\n\n            var newText = $(\"#txtAddEditRecordDataTxt\").val();\n            if (newText === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataTxt\").trigger(\"focus\");\n                return;\n            }\n\n            var splitText = divData.attr(\"data-record-split-text\");\n            var newSplitText = $(\"#chkAddEditRecordDataTxtSplitText\").prop(\"checked\");\n\n            apiUrl += \"&text=\" + encodeURIComponent(text) + \"&newText=\" + encodeURIComponent(newText) + \"&splitText=\" + splitText + \"&newSplitText=\" + newSplitText;\n            break;\n\n        case \"RP\":\n            var mailbox = divData.attr(\"data-record-mailbox\");\n\n            var newMailbox = $(\"#txtAddEditRecordDataRpMailbox\").val();\n            if (newMailbox === \"\")\n                newMailbox = \".\";\n\n            var txtDomain = divData.attr(\"data-record-txt-domain\");\n\n            var newTxtDomain = $(\"#txtAddEditRecordDataRpTxtDomain\").val();\n            if (newTxtDomain === \"\")\n                newTxtDomain = \".\";\n\n            apiUrl += \"&mailbox=\" + encodeURIComponent(mailbox) + \"&newMailbox=\" + encodeURIComponent(newMailbox) + \"&txtDomain=\" + encodeURIComponent(txtDomain) + \"&newTxtDomain=\" + encodeURIComponent(newTxtDomain);\n            break;\n\n        case \"SRV\":\n            if ($(\"#txtAddEditRecordName\").val() === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a name that includes service and protocol labels.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordName\").trigger(\"focus\");\n                return;\n            }\n\n            var priority = divData.attr(\"data-record-priority\");\n\n            var newPriority = $(\"#txtAddEditRecordDataSrvPriority\").val();\n            if (newPriority === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable priority.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvPriority\").trigger(\"focus\");\n                return;\n            }\n\n            var weight = divData.attr(\"data-record-weight\");\n\n            var newWeight = $(\"#txtAddEditRecordDataSrvWeight\").val();\n            if (newWeight === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable weight.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvWeight\").trigger(\"focus\");\n                return;\n            }\n\n            var port = divData.attr(\"data-record-port\");\n\n            var newPort = $(\"#txtAddEditRecordDataSrvPort\").val();\n            if (newPort === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable port number.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvPort\").trigger(\"focus\");\n                return;\n            }\n\n            var target = divData.attr(\"data-record-target\");\n\n            var newTarget = $(\"#txtAddEditRecordDataSrvTarget\").val();\n            if (newTarget === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value into the target field.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSrvTarget\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&priority=\" + priority + \"&newPriority=\" + newPriority + \"&weight=\" + weight + \"&newWeight=\" + newWeight + \"&port=\" + port + \"&newPort=\" + newPort + \"&target=\" + encodeURIComponent(target) + \"&newTarget=\" + encodeURIComponent(newTarget);\n            break;\n\n        case \"NAPTR\":\n            var order = divData.attr(\"data-record-order\");\n            var preference = divData.attr(\"data-record-preference\");\n            var flags = divData.attr(\"data-record-flags\");\n            var services = divData.attr(\"data-record-services\");\n            var regexp = divData.attr(\"data-record-regexp\");\n            var replacement = divData.attr(\"data-record-replacement\");\n\n            var newOrder = $(\"#txtAddEditRecordDataNaptrOrder\").val();\n            if (newOrder === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable order.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataNaptrOrder\").trigger(\"focus\");\n                return;\n            }\n\n            var newPreference = $(\"#txtAddEditRecordDataNaptrPreference\").val();\n            if (newPreference === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable preference.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataNaptrPreference\").trigger(\"focus\");\n                return;\n            }\n\n            var newFlags = $(\"#txtAddEditRecordDataNaptrFlags\").val();\n            var newServices = $(\"#txtAddEditRecordDataNaptrServices\").val();\n            var newRegexp = $(\"#txtAddEditRecordDataNaptrRegExp\").val();\n            var newReplacement = $(\"#txtAddEditRecordDataNaptrReplacement\").val();\n\n            if (newReplacement === \"\")\n                newReplacement = \".\";\n\n            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);\n            break;\n\n        case \"DNAME\":\n            var dname = $(\"#txtAddEditRecordDataValue\").val();\n            if (dname === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a domain name to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&dname=\" + encodeURIComponent(dname);\n            break;\n\n        case \"DS\":\n            var subDomainName = $(\"#txtAddEditRecordName\").val();\n            if ((subDomainName === \"\") || (subDomainName === \"@\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a name for the DS record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordName\").trigger(\"focus\");\n                return;\n            }\n\n            var keyTag = divData.attr(\"data-record-key-tag\");\n            var algorithm = divData.attr(\"data-record-algorithm\");\n            var digestType = divData.attr(\"data-record-digest-type\");\n\n            var newKeyTag = $(\"#txtAddEditRecordDataDsKeyTag\").val();\n            if (newKeyTag === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Key Tag value to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataDsKeyTag\").trigger(\"focus\");\n                return;\n            }\n\n            var newAlgorithm = $(\"#optAddEditRecordDataDsAlgorithm\").val();\n            if ((newAlgorithm === null) || (newAlgorithm === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select an DNSSEC algorithm to update the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataDsAlgorithm\").trigger(\"focus\");\n                return;\n            }\n\n            var newDigestType = $(\"#optAddEditRecordDataDsDigestType\").val();\n            if ((newDigestType === null) || (newDigestType === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Digest Type to update the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataDsDigestType\").trigger(\"focus\");\n                return;\n            }\n\n            var digest = divData.attr(\"data-record-digest\");\n\n            var newDigest = $(\"#txtAddEditRecordDataDsDigest\").val();\n            if (newDigest === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Digest hash in hex string format to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataDsDigest\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&keyTag=\" + keyTag + \"&algorithm=\" + algorithm + \"&digestType=\" + digestType + \"&newKeyTag=\" + newKeyTag + \"&newAlgorithm=\" + newAlgorithm + \"&newDigestType=\" + newDigestType + \"&digest=\" + encodeURIComponent(digest) + \"&newDigest=\" + encodeURIComponent(newDigest);\n            break;\n\n        case \"SSHFP\":\n            var sshfpAlgorithm = divData.attr(\"data-record-algorithm\");\n            var sshfpFingerprintType = divData.attr(\"data-record-fingerprint-type\");\n            var sshfpFingerprint = divData.attr(\"data-record-fingerprint\");\n\n            var newSshfpAlgorithm = $(\"#optAddEditRecordDataSshfpAlgorithm\").val();\n            if ((newSshfpAlgorithm === null) || (newSshfpAlgorithm === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select an Algorithm to update the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataSshfpAlgorithm\").trigger(\"focus\");\n                return;\n            }\n\n            var newSshfpFingerprintType = $(\"#optAddEditRecordDataSshfpFingerprintType\").val();\n            if ((newSshfpFingerprintType === null) || (newSshfpFingerprintType === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Fingerprint Type to update the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataSshfpFingerprintType\").trigger(\"focus\");\n                return;\n            }\n\n            var newSshfpFingerprint = $(\"#txtAddEditRecordDataSshfpFingerprint\").val();\n            if (newSshfpFingerprint === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Fingerprint hash in hex string format to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSshfpFingerprint\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&sshfpAlgorithm=\" + sshfpAlgorithm + \"&newSshfpAlgorithm=\" + newSshfpAlgorithm + \"&sshfpFingerprintType=\" + sshfpFingerprintType + \"&newSshfpFingerprintType=\" + newSshfpFingerprintType + \"&sshfpFingerprint=\" + encodeURIComponent(sshfpFingerprint) + \"&newSshfpFingerprint=\" + encodeURIComponent(newSshfpFingerprint);\n            break;\n\n        case \"TLSA\":\n            var tlsaCertificateUsage = divData.attr(\"data-record-certificate-usage\");\n            var tlsaSelector = divData.attr(\"data-record-selector\");\n            var tlsaMatchingType = divData.attr(\"data-record-matching-type\");\n            var tlsaCertificateAssociationData = divData.attr(\"data-record-certificate-association-data\");\n\n            var newTlsaCertificateUsage = $(\"#optAddEditRecordDataTlsaCertificateUsage\").val();\n            if ((newTlsaCertificateUsage === null) || (newTlsaCertificateUsage === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Certificate Usage to update the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataTlsaCertificateUsage\").trigger(\"focus\");\n                return;\n            }\n\n            var newTlsaSelector = $(\"#optAddEditRecordDataTlsaSelector\").val();\n            if ((newTlsaSelector === null) || (newTlsaSelector === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Selector to update the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataTlsaSelector\").trigger(\"focus\");\n                return;\n            }\n\n            var newTlsaMatchingType = $(\"#optAddEditRecordDataTlsaMatchingType\").val();\n            if ((newTlsaMatchingType === null) || (newTlsaMatchingType === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please select a Matching Type to update the record.\", divAddEditRecordAlert);\n                $(\"#optAddEditRecordDataTlsaMatchingType\").trigger(\"focus\");\n                return;\n            }\n\n            var newTlsaCertificateAssociationData = $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").val();\n            if (newTlsaCertificateAssociationData === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter the Certificate Association Data to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataTlsaCertificateAssociationData\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&tlsaCertificateUsage=\" + tlsaCertificateUsage + \"&newTlsaCertificateUsage=\" + newTlsaCertificateUsage + \"&tlsaSelector=\" + tlsaSelector + \"&newTlsaSelector=\" + newTlsaSelector + \"&tlsaMatchingType=\" + tlsaMatchingType + \"&newTlsaMatchingType=\" + newTlsaMatchingType + \"&tlsaCertificateAssociationData=\" + encodeURIComponent(tlsaCertificateAssociationData) + \"&newTlsaCertificateAssociationData=\" + encodeURIComponent(newTlsaCertificateAssociationData);\n            break;\n\n        case \"SVCB\":\n        case \"HTTPS\":\n            var svcPriority = divData.attr(\"data-record-svc-priority\");\n            var svcTargetName = divData.attr(\"data-record-svc-target-name\");\n            var svcParams = \"\";\n            {\n                var jsonSvcParams = JSON.parse(divData.attr(\"data-record-svc-params\"));\n\n                for (var paramKey in jsonSvcParams) {\n                    if (svcParams.length === 0)\n                        svcParams = paramKey + \"|\" + jsonSvcParams[paramKey];\n                    else\n                        svcParams += \"|\" + paramKey + \"|\" + jsonSvcParams[paramKey];\n                }\n\n                if (svcParams.length === 0)\n                    svcParams = false;\n            }\n\n            var newSvcPriority = $(\"#txtAddEditRecordDataSvcbPriority\").val();\n            if ((newSvcPriority === null) || (newSvcPriority === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a Priority value to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSvcbPriority\").trigger(\"focus\");\n                return;\n            }\n\n            var newSvcTargetName = $(\"#txtAddEditRecordDataSvcbTargetName\").val();\n            if ((newSvcTargetName === null) || (newSvcTargetName === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a Target Name to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataSvcbTargetName\").trigger(\"focus\");\n                return;\n            }\n\n            var newSvcParams = serializeTableData($(\"#tableAddEditRecordDataSvcbParams\"), 2, divAddEditRecordAlert);\n            if (newSvcParams === false)\n                return;\n\n            if (newSvcParams.length === 0)\n                newSvcParams = false;\n\n            var autoIpv4Hint = $(\"#chkAddEditRecordDataSvcbAutoIpv4Hint\").prop(\"checked\");\n            var autoIpv6Hint = $(\"#chkAddEditRecordDataSvcbAutoIpv6Hint\").prop(\"checked\");\n\n            apiUrl += \"&svcPriority=\" + svcPriority + \"&newSvcPriority=\" + newSvcPriority + \"&svcTargetName=\" + encodeURIComponent(svcTargetName) + \"&newSvcTargetName=\" + encodeURIComponent(newSvcTargetName) + \"&svcParams=\" + encodeURIComponent(svcParams) + \"&newSvcParams=\" + encodeURIComponent(newSvcParams) + \"&autoIpv4Hint=\" + autoIpv4Hint + \"&autoIpv6Hint=\" + autoIpv6Hint;\n            break;\n\n        case \"URI\":\n            var uriPriority = divData.attr(\"data-record-priority\");\n\n            var newUriPriority = $(\"#txtAddEditRecordDataUriPriority\").val();\n            if (newUriPriority === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable priority.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataUriPriority\").trigger(\"focus\");\n                return;\n            }\n\n            var uriWeight = divData.attr(\"data-record-weight\");\n\n            var newUriWeight = $(\"#txtAddEditRecordDataUriWeight\").val();\n            if (newUriWeight === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable weight.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataUriWeight\").trigger(\"focus\");\n                return;\n            }\n\n            var uri = divData.attr(\"data-record-uri\");\n\n            var newUri = $(\"#txtAddEditRecordDataUri\").val();\n            if (newUri === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value into the URI field.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataUri\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&uriPriority=\" + uriPriority + \"&newUriPriority=\" + newUriPriority + \"&uriWeight=\" + uriWeight + \"&newUriWeight=\" + newUriWeight + \"&uri=\" + encodeURIComponent(uri) + \"&newUri=\" + encodeURIComponent(newUri);\n            break;\n\n        case \"CAA\":\n            var flags = divData.attr(\"data-record-flags\");\n            var tag = divData.attr(\"data-record-tag\");\n\n            var newFlags = $(\"#txtAddEditRecordDataCaaFlags\").val();\n            if (newFlags === \"\")\n                newFlags = 0;\n\n            var newTag = $(\"#txtAddEditRecordDataCaaTag\").val();\n            if (newTag === \"\")\n                newTag = \"issue\";\n\n            var value = divData.attr(\"data-record-value\");\n\n            var newValue = $(\"#txtAddEditRecordDataCaaValue\").val();\n            if (newValue === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value into the authority field.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataCaaValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&flags=\" + flags + \"&tag=\" + encodeURIComponent(tag) + \"&newFlags=\" + newFlags + \"&newTag=\" + encodeURIComponent(newTag) + \"&value=\" + encodeURIComponent(value) + \"&newValue=\" + encodeURIComponent(newValue);\n            break;\n\n        case \"ANAME\":\n            var aname = divData.attr(\"data-record-aname\");\n\n            var newAName = $(\"#txtAddEditRecordDataValue\").val();\n            if (newAName === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a suitable value to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&aname=\" + encodeURIComponent(aname) + \"&newAName=\" + encodeURIComponent(newAName);\n            break;\n\n        case \"FWD\":\n            var protocol = divData.attr(\"data-record-protocol\");\n            var newProtocol = $(\"input[name=rdAddEditRecordDataForwarderProtocol]:checked\").val();\n\n            var forwarder = divData.attr(\"data-record-forwarder\");\n\n            var newForwarder = $(\"#txtAddEditRecordDataForwarder\").val();\n            if (newForwarder === \"\") {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a domain name or IP address or URL as a forwarder to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataForwarder\").trigger(\"focus\");\n                return;\n            }\n\n            var forwarderPriority = $(\"#txtAddEditRecordDataForwarderPriority\").val();\n            var dnssecValidation = $(\"#chkAddEditRecordDataForwarderDnssecValidation\").prop(\"checked\");\n\n            apiUrl += \"&protocol=\" + protocol + \"&newProtocol=\" + newProtocol + \"&forwarder=\" + encodeURIComponent(forwarder) + \"&newForwarder=\" + encodeURIComponent(newForwarder) + \"&forwarderPriority=\" + forwarderPriority + \"&dnssecValidation=\" + dnssecValidation;\n\n            if (newForwarder !== \"this-server\") {\n                var proxyType = $(\"input[name=rdAddEditRecordDataForwarderProxyType]:checked\").val();\n\n                apiUrl += \"&proxyType=\" + proxyType;\n\n                switch (proxyType) {\n                    case \"Http\":\n                    case \"Socks5\":\n                        var proxyAddress = $(\"#txtAddEditRecordDataForwarderProxyAddress\").val();\n                        var proxyPort = $(\"#txtAddEditRecordDataForwarderProxyPort\").val();\n                        var proxyUsername = $(\"#txtAddEditRecordDataForwarderProxyUsername\").val();\n                        var proxyPassword = $(\"#txtAddEditRecordDataForwarderProxyPassword\").val();\n\n                        if ((proxyAddress == null) || (proxyAddress === \"\")) {\n                            showAlert(\"warning\", \"Missing!\", \"Please enter a domain name or IP address for Proxy Server Address to update the record.\", divAddEditRecordAlert);\n                            $(\"#txtAddEditRecordDataForwarderProxyAddress\").trigger(\"focus\");\n                            return;\n                        }\n\n                        if ((proxyPort == null) || (proxyPort === \"\")) {\n                            showAlert(\"warning\", \"Missing!\", \"Please enter a port number for Proxy Server Port to update the record.\", divAddEditRecordAlert);\n                            $(\"#txtAddEditRecordDataForwarderProxyPort\").trigger(\"focus\");\n                            return;\n                        }\n\n                        apiUrl += \"&proxyAddress=\" + encodeURIComponent(proxyAddress) + \"&proxyPort=\" + proxyPort + \"&proxyUsername=\" + encodeURIComponent(proxyUsername) + \"&proxyPassword=\" + encodeURIComponent(proxyPassword);\n                        break;\n                }\n            }\n            break;\n\n        case \"APP\":\n            apiUrl += \"&appName=\" + encodeURIComponent(divData.attr(\"data-record-app-name\")) + \"&classPath=\" + encodeURIComponent(divData.attr(\"data-record-classpath\")) + \"&recordData=\" + encodeURIComponent($(\"#txtAddEditRecordDataData\").val());\n            break;\n\n        default:\n            type = $(\"#txtAddEditRecordDataUnknownType\").val();\n            var rdata = divData.attr(\"data-record-rdata\");\n\n            var newRData = $(\"#txtAddEditRecordDataValue\").val();\n            if ((newRData === null) || (newRData === \"\")) {\n                showAlert(\"warning\", \"Missing!\", \"Please enter a hex value as the RDATA to update the record.\", divAddEditRecordAlert);\n                $(\"#txtAddEditRecordDataValue\").trigger(\"focus\");\n                return;\n            }\n\n            apiUrl += \"&rdata=\" + encodeURIComponent(rdata) + \"&newRData=\" + encodeURIComponent(newRData);\n            break;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    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;\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#modalAddEditRecord\").modal(\"hide\");\n\n            //update local data\n            editZoneInfo = responseJSON.response.zone;\n            responseJSON.response.updatedRecord.index = recordIndex; //keep record index for update tasks\n            editZoneRecords[recordIndex] = responseJSON.response.updatedRecord;\n\n            if ((domain.toLowerCase() !== newDomain.toLowerCase()) && ($(\"#txtEditZoneFilterName\").val() != \"\")) {\n                //domain updated and filters applied\n                editZoneFilteredRecords = null; //to evaluate filters again\n\n                //show page\n                showEditZonePage();\n            }\n            else {\n                editZoneFilteredRecords[index] = responseJSON.response.updatedRecord;\n\n                //show updated record\n                var zoneType;\n                if (responseJSON.response.zone.internal)\n                    zoneType = \"Internal\";\n                else\n                    zoneType = responseJSON.response.zone.type;\n\n                var tableHtmlRow = getZoneRecordRowHtml(index, zone, zoneType, responseJSON.response.updatedRecord);\n                $(\"#trZoneRecord\" + index).replaceWith(tableHtmlRow);\n            }\n\n            showAlert(\"success\", \"Record Updated!\", \"Resource record was updated successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            $(\"#modalAddEditRecord\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divAddEditRecordAlert\n    });\n}\n\nfunction updateRecordState(objBtn, disable) {\n    var btn = $(objBtn);\n    var index = Number(btn.attr(\"data-id\"));\n    var divData = $(\"#data\" + index);\n\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n    var recordIndex = Number(divData.attr(\"data-record-index\"));\n    var type = divData.attr(\"data-record-type\");\n    var domain = divData.attr(\"data-record-name\");\n    var ttl = divData.attr(\"data-record-ttl\");\n    var comments = divData.attr(\"data-record-comments\");\n    var expiryTtl = $(\"#txtAddEditRecordExpiryTtl\").val();\n\n    if (domain === \"\")\n        domain = \".\";\n\n    if (disable && !confirm(\"Are you sure to disable the \" + type + \" record '\" + domain + \"'?\"))\n        return;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    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;\n\n    switch (type) {\n        case \"A\":\n        case \"AAAA\":\n            var updateSvcbHints = zoneHasSvcbAutoHint(type == \"A\", type == \"AAAA\");\n\n            apiUrl += \"&ipAddress=\" + encodeURIComponent(divData.attr(\"data-record-ip-address\")) + \"&updateSvcbHints=\" + updateSvcbHints;\n            break;\n\n        case \"NS\":\n            apiUrl += \"&nameServer=\" + encodeURIComponent(divData.attr(\"data-record-name-server\")) + \"&glue=\" + encodeURIComponent(divData.attr(\"data-record-glue\"));\n            break;\n\n        case \"CNAME\":\n            apiUrl += \"&cname=\" + encodeURIComponent(divData.attr(\"data-record-cname\"));\n            break;\n\n        case \"PTR\":\n            apiUrl += \"&ptrName=\" + encodeURIComponent(divData.attr(\"data-record-ptr-name\"));\n            break;\n\n        case \"MX\":\n            apiUrl += \"&preference=\" + divData.attr(\"data-record-preference\") + \"&exchange=\" + encodeURIComponent(divData.attr(\"data-record-exchange\"));\n            break;\n\n        case \"TXT\":\n            apiUrl += \"&text=\" + encodeURIComponent(divData.attr(\"data-record-text\")) + \"&splitText=\" + divData.attr(\"data-record-split-text\");\n            break;\n\n        case \"RP\":\n            apiUrl += \"&mailbox=\" + encodeURIComponent(divData.attr(\"data-record-mailbox\")) + \"&txtDomain=\" + encodeURIComponent(divData.attr(\"data-record-txt-domain\"));\n            break;\n\n        case \"SRV\":\n            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\"));\n            break;\n\n        case \"NAPTR\":\n            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\"));\n            break;\n\n        case \"DNAME\":\n            apiUrl += \"&dname=\" + encodeURIComponent(divData.attr(\"data-record-dname\"));\n            break;\n\n        case \"DS\":\n            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\"));\n            break;\n\n        case \"SSHFP\":\n            apiUrl += \"&sshfpAlgorithm=\" + divData.attr(\"data-record-algorithm\") + \"&sshfpFingerprintType=\" + divData.attr(\"data-record-fingerprint-type\") + \"&sshfpFingerprint=\" + encodeURIComponent(divData.attr(\"data-record-fingerprint\"));\n            break;\n\n        case \"TLSA\":\n            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\"));\n            break;\n\n        case \"SVCB\":\n        case \"HTTPS\":\n            var svcPriority = divData.attr(\"data-record-svc-priority\");\n            var svcTargetName = divData.attr(\"data-record-svc-target-name\");\n            var svcParams = \"\";\n            {\n                var jsonSvcParams = JSON.parse(divData.attr(\"data-record-svc-params\"));\n\n                for (var paramKey in jsonSvcParams) {\n                    if (svcParams.length == 0)\n                        svcParams = paramKey + \"|\" + jsonSvcParams[paramKey];\n                    else\n                        svcParams += \"|\" + paramKey + \"|\" + jsonSvcParams[paramKey];\n                }\n\n                if (svcParams.length === 0)\n                    svcParams = false;\n            }\n\n            var autoIpv4Hint = divData.attr(\"data-record-auto-ipv4hint\");\n            var autoIpv6Hint = divData.attr(\"data-record-auto-ipv6hint\");\n\n            apiUrl += \"&svcPriority=\" + svcPriority + \"&svcTargetName=\" + encodeURIComponent(svcTargetName) + \"&svcParams=\" + encodeURIComponent(svcParams) + \"&autoIpv4Hint=\" + autoIpv4Hint + \"&autoIpv6Hint=\" + autoIpv6Hint;\n            break;\n\n        case \"URI\":\n            apiUrl += \"&uriPriority=\" + divData.attr(\"data-record-priority\") + \"&uriWeight=\" + encodeURIComponent(divData.attr(\"data-record-weight\")) + \"&uri=\" + encodeURIComponent(divData.attr(\"data-record-uri\"));\n            break;\n\n        case \"CAA\":\n            apiUrl += \"&flags=\" + divData.attr(\"data-record-flags\") + \"&tag=\" + encodeURIComponent(divData.attr(\"data-record-tag\")) + \"&value=\" + encodeURIComponent(divData.attr(\"data-record-value\"));\n            break;\n\n        case \"ANAME\":\n            apiUrl += \"&aname=\" + encodeURIComponent(divData.attr(\"data-record-aname\"));\n            break;\n\n        case \"FWD\":\n            apiUrl += \"&protocol=\" + divData.attr(\"data-record-protocol\") + \"&forwarder=\" + encodeURIComponent(divData.attr(\"data-record-forwarder\"));\n\n            var proxyType = divData.attr(\"data-record-proxy-type\");\n\n            apiUrl += \"&forwarderPriority=\" + divData.attr(\"data-record-priority\") + \"&dnssecValidation=\" + divData.attr(\"data-record-dnssec-validation\") + \"&proxyType=\" + proxyType;\n\n            switch (proxyType) {\n                case \"Http\":\n                case \"Socks5\":\n                    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\"));\n                    break;\n            }\n            break;\n\n        case \"APP\":\n            apiUrl += \"&appName=\" + encodeURIComponent(divData.attr(\"data-record-app-name\")) + \"&classPath=\" + encodeURIComponent(divData.attr(\"data-record-classpath\")) + \"&recordData=\" + encodeURIComponent(divData.attr(\"data-record-data\"));\n            break;\n\n        default:\n            apiUrl += \"&rdata=\" + encodeURIComponent(divData.attr(\"data-record-rdata\"));\n            break;\n    }\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n\n            //update local data\n            editZoneInfo = responseJSON.response.zone;\n            responseJSON.response.updatedRecord.index = recordIndex; //keep record index for update tasks\n            editZoneRecords[recordIndex] = responseJSON.response.updatedRecord;\n            editZoneFilteredRecords[index] = responseJSON.response.updatedRecord;\n\n            //show updated record\n            var zoneType;\n            if (responseJSON.response.zone.internal)\n                zoneType = \"Internal\";\n            else\n                zoneType = responseJSON.response.zone.type;\n\n            var tableHtmlRow = getZoneRecordRowHtml(index, zone, zoneType, responseJSON.response.updatedRecord);\n            $(\"#trZoneRecord\" + index).replaceWith(tableHtmlRow);\n\n            if (disable)\n                showAlert(\"success\", \"Record Disabled!\", \"Resource record was disabled successfully.\");\n            else\n                showAlert(\"success\", \"Record Enabled!\", \"Resource record was enabled successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction deleteRecord(objBtn) {\n    var btn = $(objBtn);\n    var index = btn.attr(\"data-id\");\n    var divData = $(\"#data\" + index);\n\n    var zone = $(\"#titleEditZone\").attr(\"data-zone\");\n    var recordIndex = Number(divData.attr(\"data-record-index\"));\n    var domain = divData.attr(\"data-record-name\");\n    var type = divData.attr(\"data-record-type\");\n\n    if (domain === \"\")\n        domain = \".\";\n\n    if (!confirm(\"Are you sure to permanently delete the \" + type + \" record '\" + domain + \"'?\"))\n        return;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var apiUrl = \"api/zones/records/delete?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&domain=\" + encodeURIComponent(domain) + \"&type=\" + encodeURIComponent(type);\n\n    switch (type) {\n        case \"A\":\n        case \"AAAA\":\n            var updateSvcbHints = zoneHasSvcbAutoHint(type == \"A\", type == \"AAAA\");\n\n            apiUrl += \"&ipAddress=\" + encodeURIComponent(divData.attr(\"data-record-ip-address\")) + \"&updateSvcbHints=\" + updateSvcbHints;\n            break;\n\n        case \"NS\":\n            apiUrl += \"&nameServer=\" + encodeURIComponent(divData.attr(\"data-record-name-server\"));\n            break;\n\n        case \"PTR\":\n            apiUrl += \"&ptrName=\" + encodeURIComponent(divData.attr(\"data-record-ptr-name\"));\n            break;\n\n        case \"MX\":\n            apiUrl += \"&preference=\" + divData.attr(\"data-record-preference\") + \"&exchange=\" + encodeURIComponent(divData.attr(\"data-record-exchange\"));\n            break;\n\n        case \"TXT\":\n            apiUrl += \"&text=\" + encodeURIComponent(divData.attr(\"data-record-text\")) + \"&splitText=\" + divData.attr(\"data-record-split-text\");\n            break;\n\n        case \"RP\":\n            apiUrl += \"&mailbox=\" + encodeURIComponent(divData.attr(\"data-record-mailbox\")) + \"&txtDomain=\" + encodeURIComponent(divData.attr(\"data-record-txt-domain\"));\n            break;\n\n        case \"SRV\":\n            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\"));\n            break;\n\n        case \"NAPTR\":\n            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\"));\n            break;\n\n        case \"DS\":\n            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\"));\n            break;\n\n        case \"SSHFP\":\n            apiUrl += \"&sshfpAlgorithm=\" + divData.attr(\"data-record-algorithm\") + \"&sshfpFingerprintType=\" + divData.attr(\"data-record-fingerprint-type\") + \"&sshfpFingerprint=\" + encodeURIComponent(divData.attr(\"data-record-fingerprint\"));\n            break;\n\n        case \"TLSA\":\n            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\"));\n            break;\n\n        case \"SVCB\":\n        case \"HTTPS\":\n            var svcPriority = divData.attr(\"data-record-svc-priority\");\n            var svcTargetName = divData.attr(\"data-record-svc-target-name\");\n            var svcParams = \"\";\n            {\n                var jsonSvcParams = JSON.parse(divData.attr(\"data-record-svc-params\"));\n\n                for (var paramKey in jsonSvcParams) {\n                    if (svcParams.length == 0)\n                        svcParams = paramKey + \"|\" + jsonSvcParams[paramKey];\n                    else\n                        svcParams += \"|\" + paramKey + \"|\" + jsonSvcParams[paramKey];\n                }\n\n                if (svcParams.length === 0)\n                    svcParams = false;\n            }\n\n            apiUrl += \"&svcPriority=\" + svcPriority + \"&svcTargetName=\" + encodeURIComponent(svcTargetName) + \"&svcParams=\" + encodeURIComponent(svcParams);\n            break;\n\n        case \"URI\":\n            apiUrl += \"&uriPriority=\" + divData.attr(\"data-record-priority\") + \"&uriWeight=\" + encodeURIComponent(divData.attr(\"data-record-weight\")) + \"&uri=\" + encodeURIComponent(divData.attr(\"data-record-uri\"));\n            break;\n\n        case \"CAA\":\n            apiUrl += \"&flags=\" + divData.attr(\"data-record-flags\") + \"&tag=\" + encodeURIComponent(divData.attr(\"data-record-tag\")) + \"&value=\" + encodeURIComponent(divData.attr(\"data-record-value\"));\n            break;\n\n        case \"ANAME\":\n            apiUrl += \"&aname=\" + encodeURIComponent(divData.attr(\"data-record-aname\"));\n            break;\n\n        case \"FWD\":\n            apiUrl += \"&protocol=\" + divData.attr(\"data-record-protocol\") + \"&forwarder=\" + encodeURIComponent(divData.attr(\"data-record-forwarder\"));\n            break;\n\n        default:\n            var rdata = divData.attr(\"data-record-rdata\");\n            if (rdata != null)\n                apiUrl += \"&rdata=\" + encodeURIComponent(rdata);\n    }\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            //update local array\n            editZoneRecords.splice(recordIndex, 1);\n            editZoneFilteredRecords = null; //to evaluate filters again\n\n            //show page\n            showEditZonePage();\n\n            showAlert(\"success\", \"Record Deleted!\", \"Resource record was deleted successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            showPageLogin();\n        }\n    });\n}\n\nfunction showSignZoneModal(zoneName) {\n    $(\"#divDnssecSignZoneAlert\").html(\"\");\n    $(\"#lblDnssecSignZoneZoneName\").text(zoneName === \".\" ? \"<root>\" : zoneName);\n    $(\"#lblDnssecSignZoneZoneName\").attr(\"data-zone\", zoneName);\n    $(\"#rdDnssecSignZoneAlgorithmEcdsa\").prop(\"checked\", true);\n\n    $(\"#divDnssecSignZoneRsaParameters\").hide();\n    $(\"#optDnssecSignZoneRsaHashAlgorithm\").val(\"SHA256\");\n\n    $(\"#divDnssecSignZoneEcdsaParameters\").show();\n    $(\"#optDnssecSignZoneEcdsaCurve\").val(\"P256\");\n\n    $(\"#divDnssecSignZoneEddsaParameters\").hide();\n    $(\"#optDnssecSignZoneEddsaCurve\").val(\"ED25519\");\n\n    $(\"#rdDnssecSignZoneKskGenerationAutomatic\").prop(\"checked\", true)\n    $(\"#divDnssecSignZoneRsaKskKeySize\").hide();\n    $(\"#optDnssecSignZoneRsaKskKeySize\").val(\"2048\");\n    $(\"#divDnssecSignZonePemKskPrivateKey\").hide();\n    $(\"#txtDnssecSignZonePemKskPrivateKey\").val(\"\");\n\n    $(\"#rdDnssecSignZoneZskGenerationAutomatic\").prop(\"checked\", true)\n    $(\"#divDnssecSignZoneRsaZskKeySize\").hide();\n    $(\"#optDnssecSignZoneRsaZskKeySize\").val(\"1280\");\n    $(\"#divDnssecSignZonePemZskPrivateKey\").hide();\n    $(\"#txtDnssecSignZonePemZskPrivateKey\").val(\"\");\n\n    $(\"#rdDnssecSignZoneNxProofNSEC\").prop(\"checked\", true);\n\n    $(\"#divDnssecSignZoneNSEC3Parameters\").hide();\n    $(\"#txtDnssecSignZoneNSEC3Iterations\").val(\"0\");\n    $(\"#txtDnssecSignZoneNSEC3SaltLength\").val(\"0\");\n\n    $(\"#txtDnssecSignZoneDnsKeyTtl\").val(\"3600\");\n    $(\"#txtDnssecSignZoneZskAutoRollover\").val(\"30\");\n\n    $(\"#modalDnssecSignZone\").modal(\"show\");\n}\n\nfunction signPrimaryZone() {\n    var divDnssecSignZoneAlert = $(\"#divDnssecSignZoneAlert\");\n    var zone = $(\"#lblDnssecSignZoneZoneName\").attr(\"data-zone\");\n    var algorithm = $(\"input[name=rdDnssecSignZoneAlgorithm]:checked\").val();\n    var pemKskPrivateKey = $(\"#txtDnssecSignZonePemKskPrivateKey\").val();\n    var pemZskPrivateKey = $(\"#txtDnssecSignZonePemZskPrivateKey\").val();\n    var dnsKeyTtl = $(\"#txtDnssecSignZoneDnsKeyTtl\").val();\n    var zskRolloverDays = $(\"#txtDnssecSignZoneZskAutoRollover\").val();\n    var nxProof = $(\"input[name=rdDnssecSignZoneNxProof]:checked\").val();\n\n    var additionalParameters = \"\";\n\n    if (nxProof === \"NSEC3\") {\n        var iterations = $(\"#txtDnssecSignZoneNSEC3Iterations\").val();\n        var saltLength = $(\"#txtDnssecSignZoneNSEC3SaltLength\").val();\n\n        additionalParameters += \"&iterations=\" + iterations + \"&saltLength=\" + saltLength;\n    }\n\n    switch (algorithm) {\n        case \"RSA\":\n            var hashAlgorithm = $(\"#optDnssecSignZoneRsaHashAlgorithm\").val();\n            var kskKeySize = $(\"#optDnssecSignZoneRsaKskKeySize\").val();\n            var zskKeySize = $(\"#optDnssecSignZoneRsaZskKeySize\").val();\n\n            additionalParameters += \"&hashAlgorithm=\" + hashAlgorithm + \"&kskKeySize=\" + kskKeySize + \"&zskKeySize=\" + zskKeySize;\n            break;\n\n        case \"ECDSA\":\n            var curve = $(\"#optDnssecSignZoneEcdsaCurve\").val();\n\n            additionalParameters += \"&curve=\" + curve;\n            break;\n\n        case \"EDDSA\":\n            var curve = $(\"#optDnssecSignZoneEddsaCurve\").val();\n\n            additionalParameters += \"&curve=\" + curve;\n            break;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnDnssecSignZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        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),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalDnssecSignZone\").modal(\"hide\");\n\n            $(\"#txtDnssecSignZonePemKskPrivateKey\").val(\"\");\n            $(\"#txtDnssecSignZonePemZskPrivateKey\").val(\"\");\n\n            var zoneHideDnssecRecords = (localStorage.getItem(\"zoneHideDnssecRecords\") == \"true\");\n            if (zoneHideDnssecRecords) {\n                $(\"#titleEditZoneDnssecStatus\").removeClass();\n                $(\"#titleEditZoneDnssecStatus\").addClass(\"label label-primary\");\n                $(\"#titleEditZoneDnssecStatus\").show();\n\n                $(\"#lnkZoneDnssecSignZone\").hide();\n\n                $(\"#lnkZoneDnssecHideRecords\").hide();\n                $(\"#lnkZoneDnssecShowRecords\").show();\n\n                $(\"#lnkZoneDnssecViewDsRecords\").show();\n                $(\"#lnkZoneDnssecProperties\").show();\n                $(\"#lnkZoneDnssecUnsignZone\").show();\n\n                $(\"#optAddEditRecordTypeDs\").show();\n                $(\"#optAddEditRecordTypeSshfp\").show();\n                $(\"#optAddEditRecordTypeTlsa\").show();\n                $(\"#optAddEditRecordTypeAName\").hide();\n                $(\"#optAddEditRecordTypeApp\").hide();\n            }\n            else {\n                showEditZone(zone);\n            }\n\n            showAlert(\"success\", \"Zone Signed!\", \"The primary zone was signed successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalDnssecSignZone\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecSignZoneAlert\n    });\n}\n\nfunction showUnsignZoneModal(zoneName) {\n    $(\"#divDnssecUnsignZoneAlert\").html(\"\");\n    $(\"#lblDnssecUnsignZoneZoneName\").text(zoneName === \".\" ? \"<root>\" : zoneName);\n    $(\"#lblDnssecUnsignZoneZoneName\").attr(\"data-zone\", zoneName);\n\n    $(\"#modalDnssecUnsignZone\").modal(\"show\");\n}\n\nfunction unsignPrimaryZone() {\n    var divDnssecUnsignZoneAlert = $(\"#divDnssecUnsignZoneAlert\");\n    var zone = $(\"#lblDnssecUnsignZoneZoneName\").attr(\"data-zone\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnDnssecUnsignZone\");\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/unsign?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            $(\"#modalDnssecUnsignZone\").modal(\"hide\");\n\n            var zoneHideDnssecRecords = (localStorage.getItem(\"zoneHideDnssecRecords\") == \"true\");\n            if (zoneHideDnssecRecords) {\n                $(\"#titleEditZoneDnssecStatus\").hide();\n\n                $(\"#lnkZoneDnssecSignZone\").show();\n\n                $(\"#lnkZoneDnssecHideRecords\").hide();\n                $(\"#lnkZoneDnssecShowRecords\").hide();\n\n                $(\"#lnkZoneDnssecViewDsRecords\").hide();\n                $(\"#lnkZoneDnssecProperties\").hide();\n                $(\"#lnkZoneDnssecUnsignZone\").hide();\n\n                $(\"#optAddEditRecordTypeDs\").hide();\n                $(\"#optAddEditRecordTypeSshfp\").hide();\n                $(\"#optAddEditRecordTypeTlsa\").hide();\n                $(\"#optAddEditRecordTypeAName\").show();\n                $(\"#optAddEditRecordTypeApp\").show();\n            }\n            else {\n                showEditZone(zone);\n            }\n\n            showAlert(\"success\", \"Zone Unsigned!\", \"The primary zone was unsigned successfully.\");\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalDnssecUnsignZone\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecUnsignZoneAlert\n    });\n}\n\nfunction showViewDsModal(zoneName) {\n    var divDnssecViewDsAlert = $(\"#divDnssecViewDsAlert\");\n    var divDnssecViewDsLoader = $(\"#divDnssecViewDsLoader\");\n    var divDnssecViewDs = $(\"#divDnssecViewDs\");\n    var lblDnssecViewDsZoneName = $(\"#lblDnssecViewDsZoneName\");\n\n    divDnssecViewDsAlert.html(\"\");\n    lblDnssecViewDsZoneName.text(zoneName === \".\" ? \"<root>\" : zoneName);\n\n    divDnssecViewDsLoader.show();\n    divDnssecViewDs.hide();\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    $(\"#modalDnssecViewDs\").modal(\"show\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/viewDS?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zoneName) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var tableHtmlRows = \"\";\n\n            for (var i = 0; i < responseJSON.response.dsRecords.length; i++) {\n                var rowspan = responseJSON.response.dsRecords[i].digests.length + 1;\n\n                tableHtmlRows += \"<tr>\"\n                    + \"<td rowspan=\" + rowspan + \">\" + responseJSON.response.dsRecords[i].keyTag + \"</td>\"\n                    + \"<td rowspan=\" + rowspan + \">\" + responseJSON.response.dsRecords[i].dnsKeyState;\n\n                if ((responseJSON.response.dsRecords[i].dnsKeyState === \"Active\") && responseJSON.response.dsRecords[i].isRetiring)\n                    tableHtmlRows += \" (retiring)\";\n\n                if (responseJSON.response.dsRecords[i].dnsKeyStateReadyBy != null)\n                    tableHtmlRows += \"</br>(ready by: \" + moment(responseJSON.response.dsRecords[i].dnsKeyStateReadyBy).local().format(\"YYYY-MM-DD HH:mm\") + \")\";\n\n                tableHtmlRows += \"</td><td rowspan=\" + rowspan + \">\" + responseJSON.response.dsRecords[i].algorithm + \" (\" + responseJSON.response.dsRecords[i].algorithmNumber + \")</td>\";\n\n                for (var j = 0; j < responseJSON.response.dsRecords[i].digests.length; j++) {\n                    if (j > 0)\n                        tableHtmlRows += \"<tr>\";\n\n                    tableHtmlRows += \"<td>\" + responseJSON.response.dsRecords[i].digests[j].digestType + \" (\" + responseJSON.response.dsRecords[i].digests[j].digestTypeNumber + \")</td><td style=\\\"word-break: break-all;\\\">\" + responseJSON.response.dsRecords[i].digests[j].digest + \"</td>\";\n                    tableHtmlRows += \"</tr>\";\n                }\n\n                tableHtmlRows += \"<tr><td colspan=\\\"2\\\" style=\\\"word-break: break-all;\\\"><b>Public Key</b></br>\" + responseJSON.response.dsRecords[i].publicKey + \"</td></tr>\";\n            }\n\n            $(\"#tableDnssecViewDsBody\").html(tableHtmlRows);\n\n            divDnssecViewDsLoader.hide();\n            divDnssecViewDs.show();\n        },\n        error: function () {\n            divDnssecViewDsLoader.hide();\n        },\n        invalidToken: function () {\n            $(\"#modalDnssecViewDs\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecViewDsAlert,\n        objLoaderPlaceholder: divDnssecViewDsLoader\n    });\n}\n\nfunction showDnssecPropertiesModal(zoneName) {\n    var divDnssecPropertiesLoader = $(\"#divDnssecPropertiesLoader\");\n    var divDnssecProperties = $(\"#divDnssecProperties\");\n\n    $(\"#divDnssecPropertiesAlert\").html(\"\");\n    $(\"#lblDnssecPropertiesZoneName\").text(zoneName === \".\" ? \"<root>\" : zoneName);\n    $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\", zoneName);\n\n    $(\"#divDnssecPropertiesAddKey\").collapse(\"hide\");\n    $(\"#optDnssecPropertiesAddKeyKeyType\").val(\"KeySigningKey\");\n    $(\"#optDnssecPropertiesAddKeyAlgorithm\").val(\"ECDSA\");\n\n    $(\"#divDnssecPropertiesAddKeyRsaParameters\").hide();\n    $(\"#optDnssecPropertiesAddKeyRsaHashAlgorithm\").val(\"SHA256\");\n\n    $(\"#divDnssecPropertiesAddKeyEcdsaParameters\").show();\n    $(\"#optDnssecPropertiesAddKeyEcdsaCurve\").val(\"P256\");\n\n    $(\"#divDnssecPropertiesAddKeyEddsaParameters\").hide();\n    $(\"#optDnssecPropertiesAddKeyEddsaCurve\").val(\"ED25519\");\n\n    $(\"#rdDnssecPropertiesKeyGenerationAutomatic\").prop(\"checked\", true);\n    $(\"#divDnssecPropertiesAddKeyRsaKeySize\").hide();\n    $(\"#optDnssecPropertiesAddKeyRsaKeySize\").val(\"1024\");\n    $(\"#divDnssecPropertiesPemPrivateKey\").hide();\n\n    $(\"#divDnssecPropertiesAddKeyAutomaticRollover\").hide();\n    $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val(0);\n\n    divDnssecPropertiesLoader.show();\n    divDnssecProperties.hide();\n\n    $(\"#modalDnssecProperties\").modal(\"show\");\n\n    refreshDnssecProperties(divDnssecPropertiesLoader);\n}\n\nfunction refreshDnssecProperties(divDnssecPropertiesLoader) {\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n    var divDnssecPropertiesNoteReadyBy = $(\"#divDnssecPropertiesNoteReadyBy\");\n    var divDnssecPropertiesNoteActiveBy = $(\"#divDnssecPropertiesNoteActiveBy\");\n    var divDnssecPropertiesNoteRetiredRevoked = $(\"#divDnssecPropertiesNoteRetiredRevoked\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    divDnssecPropertiesNoteReadyBy.hide();\n    divDnssecPropertiesNoteActiveBy.hide();\n    divDnssecPropertiesNoteRetiredRevoked.hide();\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/properties/get?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            var tableHtmlRows = \"\";\n            var foundGeneratedKey = false;\n\n            for (var i = 0; i < responseJSON.response.dnssecPrivateKeys.length; i++) {\n                var id = Math.floor(Math.random() * 10000);\n\n                tableHtmlRows += \"<tr id=\\\"trDnssecPropertiesPrivateKey\" + id + \"\\\">\"\n                    + \"<td>\" + responseJSON.response.dnssecPrivateKeys[i].keyTag + \"</td>\"\n                    + \"<td>\" + responseJSON.response.dnssecPrivateKeys[i].keyType + \"</td>\"\n                    + \"<td>\" + responseJSON.response.dnssecPrivateKeys[i].algorithm + \" (\" + responseJSON.response.dnssecPrivateKeys[i].algorithmNumber + \")</td>\"\n                    + \"<td>\" + responseJSON.response.dnssecPrivateKeys[i].state + ((responseJSON.response.dnssecPrivateKeys[i].state === \"Active\") && responseJSON.response.dnssecPrivateKeys[i].isRetiring ? \" (retiring)\" : \"\") + \"</td>\"\n                    + \"<td>\" + moment(responseJSON.response.dnssecPrivateKeys[i].stateChangedOn).local().format(\"YYYY-MM-DD HH:mm\");\n\n                if (responseJSON.response.dnssecPrivateKeys[i].stateReadyBy != null)\n                    tableHtmlRows += \"</br>(ready by: \" + moment(responseJSON.response.dnssecPrivateKeys[i].stateReadyBy).local().format(\"YYYY-MM-DD HH:mm\") + \")\";\n                else if (responseJSON.response.dnssecPrivateKeys[i].stateActiveBy != null)\n                    tableHtmlRows += \"</br>(active by: \" + moment(responseJSON.response.dnssecPrivateKeys[i].stateActiveBy).local().format(\"YYYY-MM-DD HH:mm\") + \")\";\n\n                tableHtmlRows += \"</td><td>\";\n\n                if (responseJSON.response.dnssecPrivateKeys[i].keyType === \"ZoneSigningKey\") {\n                    switch (responseJSON.response.dnssecPrivateKeys[i].state) {\n                        case \"Generated\":\n                        case \"Published\":\n                        case \"Ready\":\n                        case \"Active\":\n                            if (responseJSON.response.dnssecPrivateKeys[i].isRetiring) {\n                                tableHtmlRows += \"-\";\n                            }\n                            else {\n                                tableHtmlRows += \"<input id=\\\"txtDnssecPropertiesPrivateKeyAutomaticRollover\" + id + \"\\\" type=\\\"text\\\" placeholder=\\\"days\\\" style=\\\"width: 40px;\\\" value=\\\"\" + responseJSON.response.dnssecPrivateKeys[i].rolloverDays + \"\\\" />\" +\n                                    \"<button type=\\\"button\\\" class=\\\"btn btn-default\\\" style=\\\"padding: 2px 6px; margin-top: -2px; margin-left: 4px; font-size: 12px; height: 26px; width: 46px;\\\" data-id=\\\"\" + id + \"\\\" data-loading-text=\\\"Save\\\" onclick=\\\"updateDnssecPrivateKey(\" + responseJSON.response.dnssecPrivateKeys[i].keyTag + \", this);\\\">Save</button>\";\n                            }\n                            break;\n\n                        default:\n                            tableHtmlRows += \"-\";\n                            break;\n                    }\n                }\n                else {\n                    tableHtmlRows += \"-\";\n                }\n\n                tableHtmlRows += \"</td>\" +\n                    \"<td align=\\\"right\\\">\";\n\n                switch (responseJSON.response.dnssecPrivateKeys[i].state) {\n                    case \"Generated\":\n                        tableHtmlRows += \"<div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDnssecPropertiesDnsKeyRowOption\" + id + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                        tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"deleteDnssecPrivateKey(\" + responseJSON.response.dnssecPrivateKeys[i].keyTag + \", '\" + id + \"'); return false;\\\">Delete</a></li>\";\n                        tableHtmlRows += \"</ul></div>\";\n                        foundGeneratedKey = true;\n                        break;\n\n                    case \"Ready\":\n                    case \"Active\":\n                        if (!responseJSON.response.dnssecPrivateKeys[i].isRetiring) {\n                            tableHtmlRows += \"<div class=\\\"dropdown\\\"><a href=\\\"#\\\" id=\\\"btnDnssecPropertiesDnsKeyRowOption\" + id + \"\\\" class=\\\"dropdown-toggle\\\" data-toggle=\\\"dropdown\\\" aria-haspopup=\\\"true\\\" aria-expanded=\\\"true\\\"><span class=\\\"glyphicon glyphicon-option-vertical\\\" aria-hidden=\\\"true\\\"></span></a><ul class=\\\"dropdown-menu dropdown-menu-right\\\">\";\n                            tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"rolloverDnssecDnsKey(\" + responseJSON.response.dnssecPrivateKeys[i].keyTag + \", '\" + id + \"'); return false;\\\">Rollover</a></li>\";\n                            tableHtmlRows += \"<li><a href=\\\"#\\\" onclick=\\\"retireDnssecDnsKey(\" + responseJSON.response.dnssecPrivateKeys[i].keyTag + \", '\" + id + \"'); return false;\\\">Retire</a></li>\";\n                            tableHtmlRows += \"</ul></div>\";\n                        }\n                        break;\n                }\n\n                tableHtmlRows += \"</td></tr>\";\n\n                if (responseJSON.response.dnssecPrivateKeys[i].keyType === \"KeySigningKey\") {\n                    switch (responseJSON.response.dnssecPrivateKeys[i].state) {\n                        case \"Published\":\n                            divDnssecPropertiesNoteReadyBy.show();\n                            break;\n\n                        case \"Ready\":\n                            divDnssecPropertiesNoteActiveBy.show();\n                            break;\n                    }\n                }\n\n                switch (responseJSON.response.dnssecPrivateKeys[i].state) {\n                    case \"Retired\":\n                    case \"Revoked\":\n                        divDnssecPropertiesNoteRetiredRevoked.show();\n                        break;\n                }\n            }\n\n            $(\"#tableDnssecPropertiesPrivateKeysBody\").html(tableHtmlRows);\n            $(\"#btnDnssecPropertiesPublishKeys\").prop(\"disabled\", !foundGeneratedKey);\n\n            switch (responseJSON.response.dnssecStatus) {\n                case \"SignedWithNSEC\":\n                    $(\"#rdDnssecPropertiesNxProofNSEC\").prop(\"checked\", true);\n\n                    $(\"#divDnssecPropertiesNSEC3Parameters\").hide();\n                    $(\"#txtDnssecPropertiesNSEC3Iterations\").val(0);\n                    $(\"#txtDnssecPropertiesNSEC3SaltLength\").val(0);\n\n                    $(\"#btnDnssecPropertiesChangeNxProof\").attr(\"data-nx-proof\", \"NSEC\");\n                    break;\n\n                case \"SignedWithNSEC3\":\n                    $(\"#rdDnssecPropertiesNxProofNSEC3\").prop(\"checked\", true);\n\n                    $(\"#divDnssecPropertiesNSEC3Parameters\").show();\n                    $(\"#txtDnssecPropertiesNSEC3Iterations\").val(responseJSON.response.nsec3Iterations);\n                    $(\"#txtDnssecPropertiesNSEC3SaltLength\").val(responseJSON.response.nsec3SaltLength);\n\n                    $(\"#btnDnssecPropertiesChangeNxProof\").attr(\"data-nx-proof\", \"NSEC3\");\n                    $(\"#btnDnssecPropertiesChangeNxProof\").attr(\"data-nsec3-iterations\", responseJSON.response.nsec3Iterations);\n                    $(\"#btnDnssecPropertiesChangeNxProof\").attr(\"data-nsec3-salt-length\", responseJSON.response.nsec3SaltLength);\n                    break;\n            }\n\n            $(\"#txtDnssecPropertiesDnsKeyTtl\").val(responseJSON.response.dnsKeyTtl);\n\n            if (divDnssecPropertiesLoader != null)\n                divDnssecPropertiesLoader.hide();\n\n            $(\"#divDnssecProperties\").show();\n        },\n        error: function () {\n            if (divDnssecPropertiesLoader != null)\n                divDnssecPropertiesLoader.hide();\n        },\n        invalidToken: function () {\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert,\n        objLoaderPlaceholder: divDnssecPropertiesLoader\n    });\n}\n\nfunction updateDnssecPrivateKey(keyTag, objBtn) {\n    var btn = $(objBtn);\n    var id = btn.attr(\"data-id\");\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n    var rolloverDays = $(\"#txtDnssecPropertiesPrivateKeyAutomaticRollover\" + id).val();\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/properties/updatePrivateKey?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&keyTag=\" + keyTag + \"&rolloverDays=\" + rolloverDays + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Updated!\", \"The DNSKEY automatic rollover config was updated successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n\nfunction deleteDnssecPrivateKey(keyTag, id) {\n    if (!confirm(\"Are you sure to permanently delete the private key (\" + keyTag + \")?\"))\n        return;\n\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnDnssecPropertiesDnsKeyRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/properties/deletePrivateKey?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&keyTag=\" + keyTag + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            $(\"#trDnssecPropertiesPrivateKey\" + id).remove();\n            showAlert(\"success\", \"Private Key Deleted!\", \"The DNSSEC private key was deleted successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n\nfunction rolloverDnssecDnsKey(keyTag, id) {\n    if (!confirm(\"Are you sure you want to rollover the DNS Key (\" + keyTag + \")?\"))\n        return;\n\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnDnssecPropertiesDnsKeyRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/properties/rolloverDnsKey?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&keyTag=\" + keyTag + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshDnssecProperties();\n            showAlert(\"success\", \"Rollover Done!\", \"The DNS Key was rolled over successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n\nfunction retireDnssecDnsKey(keyTag, id) {\n    if (!confirm(\"Are you sure you want to retire the DNS Key (\" + keyTag + \")?\"))\n        return;\n\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    var btn = $(\"#btnDnssecPropertiesDnsKeyRowOption\" + id);\n    var originalBtnHtml = btn.html();\n    btn.prop(\"disabled\", true);\n    btn.html(\"<img src='/img/loader-small.gif'/>\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/properties/retireDnsKey?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&keyTag=\" + keyTag + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshDnssecProperties();\n            showAlert(\"success\", \"DNS Key Retired!\", \"The DNS Key was retired successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.prop(\"disabled\", false);\n            btn.html(originalBtnHtml);\n        },\n        invalidToken: function () {\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n\nfunction publishAllDnssecPrivateKeys(objBtn) {\n    if (!confirm(\"Are you sure you want to publish all generated DNSSEC private keys?\"))\n        return;\n\n    var btn = $(objBtn);\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/properties/publishAllPrivateKeys?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            refreshDnssecProperties();\n            btn.button(\"reset\");\n            showAlert(\"success\", \"Keys Published!\", \"All the generated DNSSEC private keys were published successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n\nfunction addDnssecPrivateKey(objBtn) {\n    var btn = $(objBtn);\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n    var keyType = $(\"#optDnssecPropertiesAddKeyKeyType\").val();\n    var algorithm = $(\"#optDnssecPropertiesAddKeyAlgorithm\").val();\n    var pemPrivateKey = $(\"#txtDnssecPropertiesPemPrivateKey\").val();\n    var rolloverDays = $(\"#txtDnssecPropertiesAddKeyAutomaticRollover\").val();\n\n    var additionalParameters = \"\";\n\n    switch (algorithm) {\n        case \"RSA\":\n            var hashAlgorithm = $(\"#optDnssecPropertiesAddKeyRsaHashAlgorithm\").val();\n            var keySize = $(\"#optDnssecPropertiesAddKeyRsaKeySize\").val();\n\n            additionalParameters = \"&hashAlgorithm=\" + hashAlgorithm + \"&keySize=\" + keySize;\n            break;\n\n        case \"ECDSA\":\n            var curve = $(\"#optDnssecPropertiesAddKeyEcdsaCurve\").val();\n\n            additionalParameters = \"&curve=\" + curve;\n            break;\n\n        case \"EDDSA\":\n            var curve = $(\"#optDnssecPropertiesAddKeyEddsaCurve\").val();\n\n            additionalParameters = \"&curve=\" + curve;\n            break;\n    }\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        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),\n        success: function (responseJSON) {\n            $(\"#divDnssecPropertiesAddKey\").collapse(\"hide\");\n            $(\"#txtDnssecPropertiesPemPrivateKey\").val(\"\");\n\n            refreshDnssecProperties();\n            btn.button(\"reset\");\n\n            showAlert(\"success\", \"Key Added!\", \"The DNSSEC private key was added successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n\nfunction changeDnssecNxProof(objBtn) {\n    var btn = $(objBtn);\n    var currentNxProof = btn.attr(\"data-nx-proof\");\n    var currentIterations = btn.attr(\"data-nsec3-iterations\");\n    var currentSaltLength = btn.attr(\"data-nsec3-salt-length\");\n\n    var nxProof = $(\"input[name=rdDnssecPropertiesNxProof]:checked\").val();\n    var iterations;\n    var saltLength;\n\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n    var apiUrl;\n\n    switch (currentNxProof) {\n        case \"NSEC\":\n            if (nxProof === \"NSEC\") {\n                showAlert(\"success\", \"Proof Changed!\", \"The proof of non-existence was changed successfully.\", divDnssecPropertiesAlert)\n                return;\n            }\n            else {\n                var iterations = $(\"#txtDnssecPropertiesNSEC3Iterations\").val();\n                var saltLength = $(\"#txtDnssecPropertiesNSEC3SaltLength\").val();\n\n                apiUrl = \"api/zones/dnssec/properties/convertToNSEC3?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&iterations=\" + iterations + \"&saltLength=\" + saltLength;\n            }\n            break;\n\n        case \"NSEC3\":\n            if (nxProof === \"NSEC3\") {\n                iterations = $(\"#txtDnssecPropertiesNSEC3Iterations\").val();\n                saltLength = $(\"#txtDnssecPropertiesNSEC3SaltLength\").val();\n\n                if ((currentIterations == iterations) && (currentSaltLength == saltLength)) {\n                    showAlert(\"success\", \"Proof Changed!\", \"The proof of non-existence was changed successfully.\", divDnssecPropertiesAlert)\n                    return;\n                }\n                else {\n                    apiUrl = \"api/zones/dnssec/properties/updateNSEC3Params?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&iterations=\" + iterations + \"&saltLength=\" + saltLength;\n                }\n            } else {\n                apiUrl = \"api/zones/dnssec/properties/convertToNSEC?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone);\n            }\n            break;\n\n        default:\n            return;\n    }\n\n    if (!confirm(\"Are you sure you want to change the proof of non-existence options for the zone?\"))\n        return;\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: apiUrl + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.attr(\"data-nx-proof\", nxProof);\n\n            if (iterations != null)\n                btn.attr(\"data-nsec3-iterations\", iterations);\n\n            if (saltLength != null)\n                btn.attr(\"data-nsec3-salt-length\", saltLength);\n\n            btn.button(\"reset\");\n\n            var zoneHideDnssecRecords = (localStorage.getItem(\"zoneHideDnssecRecords\") == \"true\");\n            if (!zoneHideDnssecRecords)\n                showEditZone(zone);\n\n            showAlert(\"success\", \"Proof Changed!\", \"The proof of non-existence was changed successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n\nfunction updateDnssecDnsKeyTtl(objBtn) {\n    var btn = $(objBtn);\n    var divDnssecPropertiesAlert = $(\"#divDnssecPropertiesAlert\");\n    var zone = $(\"#lblDnssecPropertiesZoneName\").attr(\"data-zone\");\n    var ttl = $(\"#txtDnssecPropertiesDnsKeyTtl\").val();\n\n    var node = $(\"#optZonesClusterNode\").val();\n\n    btn.button(\"loading\");\n\n    HTTPRequest({\n        url: \"api/zones/dnssec/properties/updateDnsKeyTtl?token=\" + sessionData.token + \"&zone=\" + encodeURIComponent(zone) + \"&ttl=\" + ttl + \"&node=\" + encodeURIComponent(node),\n        success: function (responseJSON) {\n            btn.button(\"reset\");\n            showAlert(\"success\", \"TTL Updated!\", \"The DNSKEY TTL was updated successfully.\", divDnssecPropertiesAlert);\n        },\n        error: function () {\n            btn.button(\"reset\");\n        },\n        invalidToken: function () {\n            btn.button(\"reset\");\n            $(\"#modalDnssecProperties\").modal(\"hide\");\n            showPageLogin();\n        },\n        objAlertPlaceholder: divDnssecPropertiesAlert\n    });\n}\n"
  },
  {
    "path": "DnsServerCore/www/json/dnsclient-server-list-builtin.json",
    "content": "[\n\t{\n\t\t\"name\": \"Recursive Query\",\n\t\t\"addresses\": [\n\t\t\t\"recursive-resolver\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"System DNS\",\n\t\t\"addresses\": [\n\t\t\t\"system-dns\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare\",\n\t\t\"addresses\": [\n\t\t\t\"1.1.1.1\",\n\t\t\t\"1.0.0.1\",\n\t\t\t\"[2606:4700:4700::1111]\",\n\t\t\t\"[2606:4700:4700::1001]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare TLS\",\n\t\t\"addresses\": [\n\t\t\t\"cloudflare-dns.com (1.1.1.1:853)\",\n\t\t\t\"cloudflare-dns.com (1.0.0.1:853)\",\n\t\t\t\"cloudflare-dns.com ([2606:4700:4700::1111]:853)\",\n\t\t\t\"cloudflare-dns.com ([2606:4700:4700::1001]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://cloudflare-dns.com/dns-query (1.1.1.1)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google\",\n\t\t\"addresses\": [\n\t\t\t\"8.8.8.8\",\n\t\t\t\"8.8.4.4\",\n\t\t\t\"[2001:4860:4860::8888]\",\n\t\t\t\"[2001:4860:4860::8844]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.google (8.8.8.8:853)\",\n\t\t\t\"dns.google (8.8.4.4:853)\",\n\t\t\t\"dns.google ([2001:4860:4860::8888]:853)\",\n\t\t\t\"dns.google ([2001:4860:4860::8844]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.google/dns-query (8.8.8.8)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure\",\n\t\t\"addresses\": [\n\t\t\t\"9.9.9.9\",\n\t\t\t\"[2620:fe::fe]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.quad9.net (9.9.9.9:853)\",\n\t\t\t\"dns.quad9.net ([2620:fe::fe]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.quad9.net/dns-query (9.9.9.9)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS\",\n\t\t\"addresses\": [\n\t\t\t\"208.67.222.222\",\n\t\t\t\"208.67.220.220\",\n\t\t\t\"[2620:0:ccc::2]\",\n\t\t\t\"[2620:0:ccd::2]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.opendns.com (208.67.222.222:853)\",\n\t\t\t\"dns.opendns.com (208.67.220.220:853)\",\n\t\t\t\"dns.opendns.com ([2620:0:ccc::2]:853)\",\n\t\t\t\"dns.opendns.com ([2620:0:ccd::2]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://doh.opendns.com/dns-query (208.67.222.222)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard\",\n\t\t\"addresses\": [\n\t\t\t\"94.140.14.14\",\n\t\t\t\"94.140.15.15\",\n\t\t\t\"[2a10:50c0::ad1:ff]\",\n\t\t\t\"[2a10:50c0::ad2:ff]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.adguard-dns.com (94.140.14.14:853)\",\n\t\t\t\"dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.adguard-dns.com/dns-query (94.140.14.14)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard QUIC\",\n\t\t\"addresses\": [\n\t\t\t\"dns.adguard-dns.com (94.140.14.14:853)\",\n\t\t\t\"dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU\",\n\t\t\"addresses\": [\n\t\t\t\"86.54.11.1\",\n\t\t\t\"86.54.11.201\",\n\t\t\t\"[2a13:1001::86:54:11:1]\",\n\t\t\t\"[2a13:1001::86:54:11:201]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU TLS\",\n\t\t\"addresses\": [\n\t\t\t\"protective.joindns4.eu (86.54.11.1:853)\",\n\t\t\t\"protective.joindns4.eu ([2a13:1001::86:54:11:1]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://protective.joindns4.eu/dns-query (86.54.11.1)\",\n\t\t\t\"https://protective.joindns4.eu/dns-query ([2a13:1001::86:54:11:1])\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Level3\",\n\t\t\"addresses\": [\n\t\t\t\"4.2.2.1\",\n\t\t\t\"4.2.2.2\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Ultra\",\n\t\t\"addresses\": [\n\t\t\t\"156.154.70.1\",\n\t\t\t\"156.154.71.1\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Dyn\",\n\t\t\"addresses\": [\n\t\t\t\"216.146.35.35\",\n\t\t\t\"216.146.36.36\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": null,\n\t\t\"addresses\": [\n\t\t\t\"a.root-servers.net\",\n\t\t\t\"b.root-servers.net\",\n\t\t\t\"c.root-servers.net\",\n\t\t\t\"d.root-servers.net\",\n\t\t\t\"e.root-servers.net\",\n\t\t\t\"f.root-servers.net\",\n\t\t\t\"g.root-servers.net\",\n\t\t\t\"h.root-servers.net\",\n\t\t\t\"i.root-servers.net\",\n\t\t\t\"j.root-servers.net\",\n\t\t\t\"k.root-servers.net\",\n\t\t\t\"l.root-servers.net\",\n\t\t\t\"m.root-servers.net\"\n\t\t]\n\t}\n]"
  },
  {
    "path": "DnsServerCore/www/json/quick-block-lists-builtin.json",
    "content": "[\n\t{\n\t\t\"name\": \"Default\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + gambling]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + porn]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews + gambling]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews + porn]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-porn/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + gambling + porn]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-porn/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + gambling + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + porn + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn-social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews + gambling + porn]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews + gambling + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews + porn + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-porn-social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + gambling + porn + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-porn-social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Steven Black [adware + malware + fakenews + gambling + porn + social]\",\n\t\t\"urls\": [\n\t\t\t\"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OISD Big [Domains (Wildcards)]\",\n\t\t\"urls\": [\n\t\t\t\"https://big.oisd.nl/domainswild2\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OISD NSFW [Domains (Wildcards)]\",\n\t\t\"urls\": [\n\t\t\t\"https://nsfw.oisd.nl/domainswild2\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi [Multi Light - Basic protection]\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/light-onlydomains.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi [Multi Normal - All-round protection]\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/multi-onlydomains.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi [Multi PRO - Extended protection]\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/pro-onlydomains.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi [Multi PRO++ - Maximum protection]\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/pro.plus-onlydomains.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi [Multi ULTIMATE - Aggressive protection]\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/ultimate-onlydomains.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi Newly Registered Domains (NRD) - 7 days\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi Newly Registered Domains (NRD) - 14 days\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi Newly Registered Domains (NRD) - 21 days\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd21-15.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi Newly Registered Domains (NRD) - 28 days\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd21-15.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd28-22.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Hagezi Newly Registered Domains (NRD) - 35 days\",\n\t\t\"urls\": [\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd21-15.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd28-22.txt\",\n\t\t\t\"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd35-29.txt\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Shreshta - Newly Registered Domains (past week) Community Feed\",\n\t\t\"urls\": [\n\t\t\t\"https://shreshtait.com/newly-registered-domains/nrd-1w\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Shreshta - Newly Registered Domains (past month) Community Feed\",\n\t\t\"urls\": [\n\t\t\t\"https://shreshtait.com/newly-registered-domains/nrd-1m\"\n\t\t]\n\t}\n]\n"
  },
  {
    "path": "DnsServerCore/www/json/quick-forwarders-list-builtin.json",
    "content": "[\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-UDP)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"1.1.1.1\",\n\t\t\t\"1.0.0.1\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-UDP IPv6)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"[2606:4700:4700::1111]\",\n\t\t\t\"[2606:4700:4700::1001]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-TCP)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"1.1.1.1\",\n\t\t\t\"1.0.0.1\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-TCP IPv6)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"[2606:4700:4700::1111]\",\n\t\t\t\"[2606:4700:4700::1001]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-TLS)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"cloudflare-dns.com (1.1.1.1:853)\",\n\t\t\t\"cloudflare-dns.com (1.0.0.1:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-TLS IPv6)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"cloudflare-dns.com ([2606:4700:4700::1111]:853)\",\n\t\t\t\"cloudflare-dns.com ([2606:4700:4700::1001]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-HTTPS)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://cloudflare-dns.com/dns-query (1.1.1.1)\",\n\t\t\t\"https://cloudflare-dns.com/dns-query (1.0.0.1)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-HTTPS IPv6)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://cloudflare-dns.com/dns-query ([2606:4700:4700::1111])\",\n\t\t\t\"https://cloudflare-dns.com/dns-query ([2606:4700:4700::1001])\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Cloudflare (DNS-over-TOR!)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion\"\n\t\t],\n\t\t\"proxyType\": \"SOCKS5\",\n\t\t\"proxyAddress\": \"127.0.0.1\",\n\t\t\"proxyPort\": 9150,\n\t\t\"proxyUsername\": \"\",\n\t\t\"proxyPassword\": \"\"\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-UDP)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"8.8.8.8\",\n\t\t\t\"8.8.4.4\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-UDP IPv6)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"[2001:4860:4860::8888]\",\n\t\t\t\"[2001:4860:4860::8844]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-TCP)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"8.8.8.8\",\n\t\t\t\"8.8.4.4\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-TCP IPv6)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"[2001:4860:4860::8888]\",\n\t\t\t\"[2001:4860:4860::8844]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-TLS)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.google (8.8.8.8:853)\",\n\t\t\t\"dns.google (8.8.4.4:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-TLS IPv6)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.google ([2001:4860:4860::8888]:853)\",\n\t\t\t\"dns.google ([2001:4860:4860::8844]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-HTTPS)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.google/dns-query (8.8.8.8)\",\n\t\t\t\"https://dns.google/dns-query (8.8.4.4)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Google (DNS-over-HTTPS IPv6)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.google/dns-query ([2001:4860:4860::8888])\",\n\t\t\t\"https://dns.google/dns-query ([2001:4860:4860::8844])\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-UDP)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"9.9.9.9\",\n\t\t\t\"149.112.112.112\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-UDP IPv6)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"[2620:fe::fe]\",\n\t\t\t\"[2620:fe::9]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-TCP)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"9.9.9.9\",\n\t\t\t\"149.112.112.112\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-TCP IPv6)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"[2620:fe::fe]\",\n\t\t\t\"[2620:fe::9]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-TLS)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.quad9.net (9.9.9.9:853)\",\n\t\t\t\"dns.quad9.net (149.112.112.112:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-TLS IPv6)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.quad9.net ([2620:fe::fe]:853)\",\n\t\t\t\"dns.quad9.net ([2620:fe::9]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-HTTPS)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.quad9.net/dns-query (9.9.9.9)\",\n\t\t\t\"https://dns.quad9.net/dns-query (149.112.112.112)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"Quad9 Secure (DNS-over-HTTPS IPv6)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.quad9.net/dns-query ([2620:fe::fe])\",\n\t\t\t\"https://dns.quad9.net/dns-query ([2620:fe::9])\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-UDP)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"208.67.222.222\",\n\t\t\t\"208.67.220.220\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-UDP IPv6)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"[2620:0:ccc::2]\",\n\t\t\t\"[2620:0:ccd::2]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-TCP)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"208.67.222.222\",\n\t\t\t\"208.67.220.220\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-TCP IPv6)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"[2620:0:ccc::2]\",\n\t\t\t\"[2620:0:ccd::2]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-TLS)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.opendns.com (208.67.222.222:853)\",\n\t\t\t\"dns.opendns.com (208.67.220.220:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-TLS IPv6)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.opendns.com ([2620:0:ccc::2]:853)\",\n\t\t\t\"dns.opendns.com ([2620:0:ccd::2]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-HTTPS)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://doh.opendns.com/dns-query (208.67.222.222)\",\n\t\t\t\"https://doh.opendns.com/dns-query (208.67.220.220)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"OpenDNS (DNS-over-HTTPS IPv6)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://doh.opendns.com/dns-query ([2620:0:ccc::2])\",\n\t\t\t\"https://doh.opendns.com/dns-query ([2620:0:ccd::2])\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-UDP)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"94.140.14.14\",\n\t\t\t\"94.140.15.15\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-UDP IPv6)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"[2a10:50c0::ad1:ff]\",\n\t\t\t\"[2a10:50c0::ad2:ff]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-TCP)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"94.140.14.14\",\n\t\t\t\"94.140.15.15\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-TCP IPv6)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"[2a10:50c0::ad1:ff]\",\n\t\t\t\"[2a10:50c0::ad2:ff]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-TLS)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.adguard-dns.com (94.140.14.14:853)\",\n\t\t\t\"dns.adguard-dns.com (94.140.15.15:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-TLS IPv6)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)\",\n\t\t\t\"dns.adguard-dns.com ([2a10:50c0::ad2:ff]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-HTTPS)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.adguard-dns.com/dns-query (94.140.14.14)\",\n\t\t\t\"https://dns.adguard-dns.com/dns-query (94.140.15.15)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-HTTPS IPv6)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://dns.adguard-dns.com/dns-query ([2a10:50c0::ad1:ff])\",\n\t\t\t\"https://dns.adguard-dns.com/dns-query ([2a10:50c0::ad2:ff])\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-QUIC)\",\n\t\t\"protocol\": \"QUIC\",\n\t\t\"addresses\": [\n\t\t\t\"dns.adguard-dns.com (94.140.14.14:853)\",\n\t\t\t\"dns.adguard-dns.com (94.140.15.15:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"AdGuard (DNS-over-QUIC IPv6)\",\n\t\t\"protocol\": \"QUIC\",\n\t\t\"addresses\": [\n\t\t\t\"dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)\",\n\t\t\t\"dns.adguard-dns.com ([2a10:50c0::ad2:ff]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-UDP)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"86.54.11.1\",\n\t\t\t\"86.54.11.201\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-UDP IPv6)\",\n\t\t\"protocol\": \"UDP\",\n\t\t\"addresses\": [\n\t\t\t\"[2a13:1001::86:54:11:1]\",\n\t\t\t\"[2a13:1001::86:54:11:201]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-TCP)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"86.54.11.1\",\n\t\t\t\"86.54.11.201\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-TCP IPv6)\",\n\t\t\"protocol\": \"TCP\",\n\t\t\"addresses\": [\n\t\t\t\"[2a13:1001::86:54:11:1]\",\n\t\t\t\"[2a13:1001::86:54:11:201]\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-TLS)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"protective.joindns4.eu (86.54.11.1:853)\",\n\t\t\t\"protective.joindns4.eu (86.54.11.201:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-TLS IPv6)\",\n\t\t\"protocol\": \"TLS\",\n\t\t\"addresses\": [\n\t\t\t\"protective.joindns4.eu ([2a13:1001::86:54:11:1]:853)\",\n\t\t\t\"protective.joindns4.eu ([2a13:1001::86:54:11:201]:853)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-HTTPS)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://protective.joindns4.eu/dns-query (86.54.11.1)\",\n\t\t\t\"https://protective.joindns4.eu/dns-query (86.54.11.201)\"\n\t\t]\n\t},\n\t{\n\t\t\"name\": \"DNS4EU (DNS-over-HTTPS IPv6)\",\n\t\t\"protocol\": \"HTTPS\",\n\t\t\"addresses\": [\n\t\t\t\"https://protective.joindns4.eu/dns-query ([2a13:1001::86:54:11:1])\",\n\t\t\t\"https://protective.joindns4.eu/dns-query ([2a13:1001::86:54:11:201])\"\n\t\t]\n\t}\n]"
  },
  {
    "path": "DnsServerCore/www/json/readme.txt",
    "content": "READ ME\n=======\n\nThis 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.\n\nYou 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.\n\nFor 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.\n\nNote! Once the custom list file is saved, you will need to refresh the web app so that it loads the updated custom list.\n\nWarning! 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.\n"
  },
  {
    "path": "DnsServerCore/www/robots.txt",
    "content": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/DnsServerCore.ApplicationCommon.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <Authors>Shreyas Zare</Authors>\n    <Company>Technitium</Company>\n    <Product>Technitium DNS Server</Product>\n    <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n    <RepositoryType></RepositoryType>\n    <Description></Description>\n    <PackageId>DnsServerCore.ApplicationCommon</PackageId>\n    <Version>9.0</Version>\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Reference Include=\"TechnitiumLibrary.Net\">\n      <HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n      <Private>false</Private>\n    </Reference>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsAppRecordRequestHandler.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2023  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// Allows a DNS App to handle incoming DNS requests for configured APP records in the DNS server zones.\n    /// </summary>\n    public interface IDnsAppRecordRequestHandler\n    {\n        /// <summary>\n        /// Allows a DNS App to respond to the incoming DNS requests for an APP record in a primary or secondary zone.\n        /// </summary>\n        /// <param name=\"request\">The incoming DNS request to be processed.</param>\n        /// <param name=\"remoteEP\">The end point (IP address and port) of the client making the request.</param>\n        /// <param name=\"protocol\">The protocol using which the request was received.</param>\n        /// <param name=\"isRecursionAllowed\">Tells if the DNS server is configured to allow recursion for the client making this request.</param>\n        /// <param name=\"zoneName\">The name of the application zone that the APP record belongs to.</param>\n        /// <param name=\"appRecordName\">The domain name of the APP record.</param>\n        /// <param name=\"appRecordTtl\">The TTL value set in the APP record.</param>\n        /// <param name=\"appRecordData\">The record data in the APP record as required for processing the request.</param>\n        /// <returns>The DNS response for the DNS request or <c>null</c> to send NODATA response when QNAME matches APP record name or else NXDOMAIN response with an SOA authority.</returns>\n        Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData);\n\n        /// <summary>\n        /// 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 <c>null</c> if no APP record data is required by the app.\n        /// </summary>\n        string ApplicationRecordDataTemplate { get; }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsApplication.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// Allows an application to initialize itself using the DNS app config.\n    /// </summary>\n    public interface IDnsApplication : IDisposable\n    {\n        /// <summary>\n        /// Allows initializing the DNS application with a config. This function is also called when the config is updated to allow reloading.\n        /// </summary>\n        /// <param name=\"dnsServer\">The DNS server interface object that allows access to DNS server properties.</param>\n        /// <param name=\"config\">The DNS application config stored in the <c>dnsApp.config</c> file.</param>\n        Task InitializeAsync(IDnsServer dnsServer, string config);\n\n        /// <summary>\n        /// The description about this app to be shown in the Apps section of the DNS web console.\n        /// </summary>\n        string Description { get; }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsApplicationPreference.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// 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.\n    /// </summary>\n    public interface IDnsApplicationPreference\n    {\n        /// <summary>\n        /// Returns the preference value configured for the application.\n        /// </summary>\n        byte Preference { get; }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsAuthoritativeRequestHandler.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// 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.\n    /// </summary>\n    public interface IDnsAuthoritativeRequestHandler\n    {\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"request\">The incoming DNS request to be processed.</param>\n        /// <param name=\"remoteEP\">The end point (IP address and port) of the client making the request.</param>\n        /// <param name=\"protocol\">The protocol using which the request was received.</param>\n        /// <param name=\"isRecursionAllowed\">Tells if the DNS server is configured to allow recursion for the client making this request.</param>\n        /// <returns>The DNS response for the DNS request or <c>null</c> to let the DNS server core process the request as usual.</returns>\n        Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed);\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsPostProcessor.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// 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.\n    /// </summary>\n    public interface IDnsPostProcessor\n    {\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"request\">The incoming DNS request received by the DNS server.</param>\n        /// <param name=\"remoteEP\">The end point (IP address and port) of the client making the request.</param>\n        /// <param name=\"protocol\">The protocol using which the request was received.</param>\n        /// <param name=\"response\">The DNS response that was generated by the DNS server for the received DNS request or from another DNS app that performed post processing.</param>\n        /// <returns>The DNS response that the DNS server should send to the client or <c>null</c> to drop the DNS request.</returns>\n        Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response);\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsQueryLogger.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    public enum DnsServerResponseType : byte\n    {\n        Authoritative = 1,\n        Recursive = 2,\n        Cached = 3,\n        Blocked = 4,\n        UpstreamBlocked = 5,\n        UpstreamBlockedCached = 6,\n        Dropped = 7\n    }\n\n    /// <summary>\n    /// Allows a DNS App to log incoming DNS requests and their corresponding responses.\n    /// </summary>\n    public interface IDnsQueryLogger\n    {\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"timestamp\">The time stamp of the log entry.</param>\n        /// <param name=\"request\">The incoming DNS request that was received.</param>\n        /// <param name=\"remoteEP\">The end point (IP address and port) of the client making the request.</param>\n        /// <param name=\"protocol\">The protocol using which the request was received.</param>\n        /// <param name=\"response\">The DNS response that was sent.</param>\n        Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response);\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsQueryLogs.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Dns.ResourceRecords;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// 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.\n    /// </summary>\n    public interface IDnsQueryLogs\n    {\n        /// <summary>\n        /// Allows DNS Server HTTP API to query the logs recorded by the DNS App.\n        /// </summary>\n        /// <param name=\"pageNumber\">The page number to be displayed to the user.</param>\n        /// <param name=\"entriesPerPage\">Total entries per page.</param>\n        /// <param name=\"descendingOrder\">Lists log entries in descending order.</param>\n        /// <param name=\"start\">Optional parameter to filter records by start date time.</param>\n        /// <param name=\"end\">Optional parameter to filter records by end date time.</param>\n        /// <param name=\"clientIpAddress\">Optional parameter to filter records by the client IP address.</param>\n        /// <param name=\"protocol\">Optional parameter to filter records by the DNS transport protocol.</param>\n        /// <param name=\"responseType\">Optional parameter to filter records by the type of response.</param>\n        /// <param name=\"rcode\">Optional parameter to filter records by the response code.</param>\n        /// <param name=\"qname\">Optional parameter to filter records by the request QNAME.</param>\n        /// <param name=\"qtype\">Optional parameter to filter records by the request QTYPE.</param>\n        /// <param name=\"qclass\">Optional parameter to filter records by the request QCLASS.</param>\n        /// <returns>The <code>DnsLogPage</code> object that contains all the entries in the requested page number.</returns>\n        Task<DnsLogPage> 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);\n    }\n\n    public class DnsLogPage\n    {\n        #region variables\n\n        readonly long _pageNumber;\n        readonly long _totalPages;\n        readonly long _totalEntries;\n        readonly IReadOnlyList<DnsLogEntry> _entries;\n\n        #endregion\n\n        #region constructor\n\n        /// <summary>\n        /// Creates a new object initialized with all the log page parameters.\n        /// </summary>\n        /// <param name=\"pageNumber\">The actual page number of the selected data set.</param>\n        /// <param name=\"totalPages\">The total pages for the selected data set.</param>\n        /// <param name=\"totalEntries\">The total number of entries in the selected data set.</param>\n        /// <param name=\"entries\">The DNS log entries in this page.</param>\n        public DnsLogPage(long pageNumber, long totalPages, long totalEntries, IReadOnlyList<DnsLogEntry> entries)\n        {\n            _pageNumber = pageNumber;\n            _totalPages = totalPages;\n            _totalEntries = totalEntries;\n            _entries = entries;\n        }\n\n        #endregion\n\n        #region properties\n\n        /// <summary>\n        /// The actual page number of the selected data set.\n        /// </summary>\n        public long PageNumber\n        { get { return _pageNumber; } }\n\n        /// <summary>\n        /// The total pages for the selected data set.\n        /// </summary>\n        public long TotalPages\n        { get { return _totalPages; } }\n\n        /// <summary>\n        /// The total number of entries in the selected data set.\n        /// </summary>\n        public long TotalEntries\n        { get { return _totalEntries; } }\n\n        /// <summary>\n        /// The DNS log entries in this page.\n        /// </summary>\n        public IReadOnlyList<DnsLogEntry> Entries\n        { get { return _entries; } }\n\n        #endregion\n    }\n\n    public class DnsLogEntry\n    {\n        #region variables\n\n        readonly long _rowNumber;\n        readonly DateTime _timestamp;\n        readonly IPAddress _clientIpAddress;\n        readonly DnsTransportProtocol _protocol;\n        readonly DnsServerResponseType _responseType;\n        readonly double? _responseRtt;\n        readonly DnsResponseCode _rcode;\n        readonly DnsQuestionRecord _question;\n        readonly string _answer;\n\n        #endregion\n\n        #region constructor\n\n        /// <summary>\n        /// Creates a new object initialized with all the log entry parameters.\n        /// </summary>\n        /// <param name=\"rowNumber\">The row number of the entry in the selected data set.</param>\n        /// <param name=\"timestamp\">The time stamp of the log entry.</param>\n        /// <param name=\"clientIpAddress\">The client IP address of the request.</param>\n        /// <param name=\"protocol\">The DNS transport protocol of the request.</param>\n        /// <param name=\"responseType\">The type of response sent by the DNS server.</param>\n        /// <param name=\"responseRtt\">The round trip time taken to resolve the request.</param>\n        /// <param name=\"rcode\">The response code sent by the DNS server.</param>\n        /// <param name=\"question\">The question section in the request.</param>\n        /// <param name=\"answer\">The answer in text format sent by the DNS server.</param>\n        public DnsLogEntry(long rowNumber, DateTime timestamp, IPAddress clientIpAddress, DnsTransportProtocol protocol, DnsServerResponseType responseType, double? responseRtt, DnsResponseCode rcode, DnsQuestionRecord question, string answer)\n        {\n            _rowNumber = rowNumber;\n            _timestamp = timestamp;\n            _clientIpAddress = clientIpAddress;\n            _protocol = protocol;\n            _responseType = responseType;\n            _responseRtt = responseRtt;\n            _rcode = rcode;\n            _question = question;\n            _answer = answer;\n\n            switch (_timestamp.Kind)\n            {\n                case DateTimeKind.Local:\n                    _timestamp = _timestamp.ToUniversalTime();\n                    break;\n\n                case DateTimeKind.Unspecified:\n                    _timestamp = DateTime.SpecifyKind(_timestamp, DateTimeKind.Utc);\n                    break;\n            }\n        }\n\n        /// <summary>\n        /// Creates a new object initialized with all the log entry parameters.\n        /// </summary>\n        /// <param name=\"rowNumber\">The row number of the entry in the selected data set.</param>\n        /// <param name=\"timestamp\">The time stamp of the log entry.</param>\n        /// <param name=\"clientIpAddress\">The client IP address of the request.</param>\n        /// <param name=\"protocol\">The DNS transport protocol of the request.</param>\n        /// <param name=\"responseType\">The type of response sent by the DNS server.</param>\n        /// <param name=\"rcode\">The response code sent by the DNS server.</param>\n        /// <param name=\"question\">The question section in the request.</param>\n        /// <param name=\"answer\">The answer in text format sent by the DNS server.</param>\n        public DnsLogEntry(long rowNumber, DateTime timestamp, IPAddress clientIpAddress, DnsTransportProtocol protocol, DnsServerResponseType responseType, DnsResponseCode rcode, DnsQuestionRecord question, string answer)\n        {\n            _rowNumber = rowNumber;\n            _timestamp = timestamp;\n            _clientIpAddress = clientIpAddress;\n            _protocol = protocol;\n            _responseType = responseType;\n            _rcode = rcode;\n            _question = question;\n            _answer = answer;\n\n            switch (_timestamp.Kind)\n            {\n                case DateTimeKind.Local:\n                    _timestamp = _timestamp.ToUniversalTime();\n                    break;\n\n                case DateTimeKind.Unspecified:\n                    _timestamp = DateTime.SpecifyKind(_timestamp, DateTimeKind.Utc);\n                    break;\n            }\n        }\n\n        #endregion\n\n        #region properties\n\n        /// <summary>\n        /// The row number of the entry in the selected data set.\n        /// </summary>\n        public long RowNumber\n        { get { return _rowNumber; } }\n\n        /// <summary>\n        /// The time stamp of the log entry.\n        /// </summary>\n        public DateTime Timestamp\n        { get { return _timestamp; } }\n\n        /// <summary>\n        /// The client IP address of the request.\n        /// </summary>\n        public IPAddress ClientIpAddress\n        { get { return _clientIpAddress; } }\n\n        /// <summary>\n        /// The DNS transport protocol of the request.\n        /// </summary>\n        public DnsTransportProtocol Protocol\n        { get { return _protocol; } }\n\n        /// <summary>\n        /// The type of response sent by the DNS server.\n        /// </summary>\n        public DnsServerResponseType ResponseType\n        { get { return _responseType; } }\n\n        /// <summary>\n        /// The round trip time taken to resolve the request.\n        /// </summary>\n        public double? ResponseRtt\n        { get { return _responseRtt; } }\n\n        /// <summary>\n        /// The response code sent by the DNS server.\n        /// </summary>\n        public DnsResponseCode RCODE\n        { get { return _rcode; } }\n\n        /// <summary>\n        /// The question section in the request.\n        /// </summary>\n        public DnsQuestionRecord Question\n        { get { return _question; } }\n\n        /// <summary>\n        /// The answer in text format sent by the DNS server.\n        /// </summary>\n        public string Answer\n        { get { return _answer; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsRequestBlockingHandler.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2023  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// Lets DNS Apps provide DNS level domain name blocking feature.\n    /// </summary>\n    public interface IDnsRequestBlockingHandler\n    {\n        /// <summary>\n        /// 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).\n        /// </summary>\n        /// <param name=\"request\">The incoming DNS request to be processed.</param>\n        /// <param name=\"remoteEP\">The end point (IP address and port) of the client making the request.</param>\n        /// <returns>Returns <c>true</c> if the query domain name in the incoming DNS request is allowed to bypass blocking.</returns>\n        Task<bool> IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP);\n\n        /// <summary>\n        /// Specifies if the query domain name in the incoming DNS request is blocked based on the app's own configured block lists.\n        /// </summary>\n        /// <param name=\"request\">The incoming DNS request to be processed.</param>\n        /// <param name=\"remoteEP\">The end point (IP address and port) of the client making the request.</param>\n        /// <returns>The blocked DNS response for the DNS request or <c>null</c> to let the DNS server core process the request as usual.</returns>\n        Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP);\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsRequestController.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Net;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    public enum DnsRequestControllerAction\n    {\n        /// <summary>\n        /// Allow the request to be processed.\n        /// </summary>\n        Allow = 0,\n\n        /// <summary>\n        /// Drop the request without any response.\n        /// </summary>\n        DropSilently = 1,\n\n        /// <summary>\n        /// Drop the request with a Refused response.\n        /// </summary>\n        DropWithRefused = 2\n    }\n\n    /// <summary>\n    /// Allows a DNS App to inspect and optionally drop incoming DNS requests before they are processed by the DNS Server core.\n    /// </summary>\n    public interface IDnsRequestController\n    {\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"request\">The incoming DNS request.</param>\n        /// <param name=\"remoteEP\">The end point (IP address and port) of the client making the request.</param>\n        /// <param name=\"protocol\">The protocol using which the request was received.</param>\n        /// <returns>The action that must be taken by the DNS server i.e. if the request must be allowed or dropped.</returns>\n        Task<DnsRequestControllerAction> GetRequestActionAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol);\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.ApplicationCommon/IDnsServer.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Net.Mail;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace DnsServerCore.ApplicationCommon\n{\n    /// <summary>\n    /// Provides an interface to access the internal DNS Server core.\n    /// </summary>\n    public interface IDnsServer : IDnsClient\n    {\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"question\">The question record containing the details to query.</param>\n        /// <param name=\"timeout\">The timeout value in milliseconds to wait for response.</param>\n        /// <param name=\"cancellationToken\">The cancellation token to cancel the operation.</param>\n        /// <returns>The DNS response for the DNS query.</returns>\n        /// <exception cref=\"TimeoutException\">When request times out.</exception>\n        Task<DnsDatagram> DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, CancellationToken cancellationToken = default);\n\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"request\">The DNS request to query.</param>\n        /// <param name=\"timeout\">The timeout value in milliseconds to wait for response.</param>\n        /// <param name=\"cancellationToken\">The cancellation token to cancel the operation.</param>\n        /// <returns>The DNS response for the DNS query.</returns>\n        /// <exception cref=\"TimeoutException\">When request times out.</exception>\n        Task<DnsDatagram> DirectQueryAsync(DnsDatagram request, int timeout = 4000, CancellationToken cancellationToken = default);\n\n        /// <summary>\n        /// Writes a log entry to the DNS server log file.\n        /// </summary>\n        /// <param name=\"message\">The message to log.</param>\n        void WriteLog(string message);\n\n        /// <summary>\n        /// Writes a log entry to the DNS server log file.\n        /// </summary>\n        /// <param name=\"ex\">The exception to log.</param>\n        void WriteLog(Exception ex);\n\n        /// <summary>\n        /// The name of this installed application.\n        /// </summary>\n        string ApplicationName { get; }\n\n        /// <summary>\n        /// The folder where this application is saved on the disk. Can be used to create temp files, read/write files, etc. for this application.\n        /// </summary>\n        string ApplicationFolder { get; }\n\n        /// <summary>\n        /// The primary domain name used by this DNS Server to identify itself.\n        /// </summary>\n        string ServerDomain { get; }\n\n        /// <summary>\n        /// The default responsible person email address for this DNS Server.\n        /// </summary>\n        MailAddress ResponsiblePerson { get; }\n\n        /// <summary>\n        /// The DNS cache object which provides direct access to the DNS server cache.\n        /// </summary>\n        IDnsCache DnsCache { get; }\n\n        /// <summary>\n        /// The proxy server setting on the DNS server to be used when required to make any outbound network connection.\n        /// </summary>\n        NetProxy Proxy { get; }\n\n        /// <summary>\n        /// Tells if the DNS server prefers using IPv6 as per the settings.\n        /// </summary>\n        bool PreferIPv6 { get; }\n\n        /// <summary>\n        /// Returns the UDP payload size configured in the settings.\n        /// </summary>\n        public ushort UdpPayloadSize { get; }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.HttpApi/DnsServerCore.HttpApi.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<RepositoryType></RepositoryType>\n\t\t<Description></Description>\n\t\t<PackageId>DnsServerCore.HttpApi</PackageId>\n\t\t<Version>3.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Nullable>enable</Nullable>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.dll</HintPath>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t\t<Private>false</Private>\n\t\t</Reference>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "DnsServerCore.HttpApi/HttpApiClient.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore.HttpApi.Models;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing System;\nusing System.Buffers.Text;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Security;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.Web;\nusing TechnitiumLibrary;\nusing TechnitiumLibrary.Net.Dns;\nusing TechnitiumLibrary.Net.Http.Client;\nusing TechnitiumLibrary.Net.Proxy;\n\nnamespace DnsServerCore.HttpApi\n{\n    public sealed class HttpApiClient : IDisposable\n    {\n        #region variables\n\n        readonly static JsonSerializerOptions _serializerOptions;\n\n        readonly Uri _serverUrl;\n        string? _token;\n\n        readonly HttpClient _httpClient;\n        bool _loggedIn;\n\n        #endregion\n\n        #region constructor\n\n        static HttpApiClient()\n        {\n            _serializerOptions = new JsonSerializerOptions();\n            _serializerOptions.PropertyNameCaseInsensitive = true;\n        }\n\n        public HttpApiClient(string serverUrl, NetProxy? proxy = null, bool preferIPv6 = false, bool ignoreCertificateErrors = false, IDnsClient? dnsClient = null)\n            : this(new Uri(serverUrl), proxy, preferIPv6, ignoreCertificateErrors, dnsClient)\n        { }\n\n        public HttpApiClient(Uri serverUrl, NetProxy? proxy = null, bool preferIPv6 = false, bool ignoreCertificateErrors = false, IDnsClient? dnsClient = null)\n        {\n            _serverUrl = serverUrl;\n\n            HttpClientNetworkHandler handler = new HttpClientNetworkHandler();\n            handler.Proxy = proxy;\n            handler.NetworkType = preferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;\n            handler.DnsClient = dnsClient;\n\n            if (ignoreCertificateErrors)\n            {\n                handler.InnerHandler.SslOptions.RemoteCertificateValidationCallback = delegate (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)\n                {\n                    return true;\n                };\n            }\n            else\n            {\n                handler.EnableDANE = true;\n            }\n\n            _httpClient = new HttpClient(handler);\n            _httpClient.BaseAddress = _serverUrl;\n            _httpClient.DefaultRequestHeaders.Add(\"user-agent\", \"Technitium DNS Server HTTP API Client\");\n            _httpClient.Timeout = TimeSpan.FromSeconds(30);\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        bool _disposed;\n\n        public void Dispose()\n        {\n            if (_disposed)\n                return;\n\n            _httpClient?.Dispose();\n\n            _disposed = true;\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n\n        #region private\n\n        private static void CheckResponseStatus(JsonElement rootElement)\n        {\n            if (!rootElement.TryGetProperty(\"status\", out JsonElement jsonStatus))\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            string? status = jsonStatus.GetString()?.ToLowerInvariant();\n            switch (status)\n            {\n                case \"ok\":\n                    return;\n\n                case \"error\":\n                    {\n                        Exception? innerException = null;\n\n                        if (rootElement.TryGetProperty(\"innerErrorMessage\", out JsonElement jsonInnerErrorMessage))\n                            innerException = new HttpApiClientException(jsonInnerErrorMessage.GetString()!);\n\n                        if (rootElement.TryGetProperty(\"errorMessage\", out JsonElement jsonErrorMessage))\n                        {\n                            if (innerException is null)\n                                throw new HttpApiClientException(jsonErrorMessage.GetString()!);\n\n                            throw new HttpApiClientException(jsonErrorMessage.GetString()!, innerException);\n                        }\n\n                        throw new HttpApiClientException();\n                    }\n\n                case \"invalid-token\":\n                    {\n                        if (rootElement.TryGetProperty(\"errorMessage\", out JsonElement jsonErrorMessage))\n                            throw new InvalidTokenHttpApiClientException(jsonErrorMessage.GetString()!);\n\n                        throw new InvalidTokenHttpApiClientException();\n                    }\n\n                case \"2fa-required\":\n                    {\n                        if (rootElement.TryGetProperty(\"errorMessage\", out JsonElement jsonErrorMessage))\n                            throw new TwoFactorAuthRequiredHttpApiClientException(jsonErrorMessage.GetString()!);\n\n                        throw new TwoFactorAuthRequiredHttpApiClientException();\n                    }\n\n                default:\n                    throw new HttpApiClientException(\"Unknown status value was received: \" + status);\n            }\n        }\n\n        #endregion\n\n        #region public\n\n        public async Task<SessionInfo> LoginAsync(string username, string password, string? totp = null, bool includeInfo = false, CancellationToken cancellationToken = default)\n        {\n            if (_loggedIn)\n                throw new HttpApiClientException(\"Already logged in.\");\n\n            HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(_serverUrl, $\"api/user/login\"));\n\n            Dictionary<string, string> parameters = new Dictionary<string, string>\n            {\n                { \"user\", username },\n                { \"pass\", password },\n                { \"includeInfo\", includeInfo.ToString() }\n            };\n\n            if (totp is not null)\n                parameters.Add(\"totp\", totp);\n\n            httpRequest.Content = new FormUrlEncodedContent(parameters);\n\n            HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            SessionInfo? sessionInfo = rootElement.Deserialize<SessionInfo>(_serializerOptions);\n            if (sessionInfo is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            _token = sessionInfo.Token;\n            _loggedIn = true;\n\n            return sessionInfo;\n        }\n\n        public async Task LogoutAsync(CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exist to logout.\");\n\n            Stream stream = await _httpClient.GetStreamAsync($\"api/user/logout?token={_token}\", cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            _token = null;\n            _loggedIn = false;\n        }\n\n        public void UseApiToken(string token)\n        {\n            if (_loggedIn)\n                throw new HttpApiClientException(\"Already logged in. Please logout before using a different API token.\");\n\n            _token = token;\n            _loggedIn = true;\n        }\n\n        public async Task<DashboardStats> 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)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            string path = $\"api/dashboard/stats/get?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&type={type}&utc={utcFormat}&dontTrimQueryTypeData={dontTrimQueryTypeData}\";\n\n            if (type == DashboardStatsType.Custom)\n                path += $\"&start={startDate:O}&end={endDate:O}\";\n\n            HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(_serverUrl, path));\n            httpRequest.Headers.Add(\"Accept-Language\", acceptLanguage);\n\n            HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            DashboardStats? stats = rootElement.GetProperty(\"response\").Deserialize<DashboardStats>(_serializerOptions);\n            if (stats is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return stats;\n        }\n\n        public async Task<DashboardStats> GetDashboardTopStatsAsync(string actingUsername, DashboardTopStatsType statsType, int limit = 1000, DashboardStatsType type = DashboardStatsType.LastHour, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            string path = $\"api/dashboard/stats/getTop?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&type={type}&statsType={statsType}&limit={limit}\";\n\n            if (type == DashboardStatsType.Custom)\n                path += $\"&start={startDate:O}&end={endDate:O}\";\n\n            HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(_serverUrl, path));\n\n            HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            DashboardStats? stats = rootElement.GetProperty(\"response\").Deserialize<DashboardStats>(_serializerOptions);\n            if (stats is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return stats;\n        }\n\n        public async Task SetClusterSettingsAsync(string actingUsername, IReadOnlyDictionary<string, string> clusterParameters, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            if (clusterParameters.Count == 0)\n                throw new ArgumentException(\"At least one parameter must be provided.\", nameof(clusterParameters));\n\n            foreach (KeyValuePair<string, string> parameter in clusterParameters)\n            {\n                switch (parameter.Key)\n                {\n                    case \"token\":\n                    case \"node\":\n                        throw new ArgumentException($\"The '{parameter.Key}' is an invalid Settings parameter.\", nameof(clusterParameters));\n                }\n            }\n\n            HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(_serverUrl, $\"api/settings/set?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}\"));\n\n            httpRequest.Content = new FormUrlEncodedContent(clusterParameters);\n\n            HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n        }\n\n        public async Task ForceUpdateBlockListsAsync(string actingUsername, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            Stream stream = await _httpClient.GetStreamAsync($\"api/settings/forceUpdateBlockLists?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}\", cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n        }\n\n        public async Task TemporaryDisableBlockingAsync(string actingUsername, int minutes, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            Stream stream = await _httpClient.GetStreamAsync($\"api/settings/temporaryDisableBlocking?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&minutes={minutes}\", cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n        }\n\n        public async Task<ClusterInfo> GetClusterStateAsync(bool includeServerIpAddresses = false, bool includeNodeCertificates = false, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            Stream stream = await _httpClient.GetStreamAsync($\"api/admin/cluster/state?token={_token}&includeServerIpAddresses={includeServerIpAddresses}&includeNodeCertificates={includeNodeCertificates}\", cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            ClusterInfo? clusterInfo = rootElement.GetProperty(\"response\").Deserialize<ClusterInfo>(_serializerOptions);\n            if (clusterInfo is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return clusterInfo;\n        }\n\n        public async Task<ClusterInfo> DeleteClusterAsync(bool forceDelete = false, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            Stream stream = await _httpClient.GetStreamAsync($\"api/admin/cluster/primary/delete?token={_token}&forceDelete={forceDelete}\", cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            ClusterInfo? clusterInfo = rootElement.GetProperty(\"response\").Deserialize<ClusterInfo>(_serializerOptions);\n            if (clusterInfo is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return clusterInfo;\n        }\n\n        public async Task<ClusterInfo> JoinClusterAsync(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyCollection<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            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);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            ClusterInfo? clusterInfo = rootElement.GetProperty(\"response\").Deserialize<ClusterInfo>(_serializerOptions);\n            if (clusterInfo is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return clusterInfo;\n        }\n\n        public async Task<ClusterInfo> DeleteSecondaryNodeAsync(int secondaryNodeId, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            Stream stream = await _httpClient.GetStreamAsync($\"api/admin/cluster/primary/deleteSecondary?token={_token}&secondaryNodeId={secondaryNodeId}\", cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            ClusterInfo? clusterInfo = rootElement.GetProperty(\"response\").Deserialize<ClusterInfo>(_serializerOptions);\n            if (clusterInfo is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return clusterInfo;\n        }\n\n        public async Task<ClusterInfo> UpdateSecondaryNodeAsync(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyCollection<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            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);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            ClusterInfo? clusterInfo = rootElement.GetProperty(\"response\").Deserialize<ClusterInfo>(_serializerOptions);\n            if (clusterInfo is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return clusterInfo;\n        }\n\n        public async Task<(Stream, DateTime)> TransferConfigFromPrimaryNodeAsync(DateTime ifModifiedSince = default, IReadOnlyCollection<string>? includeZones = null, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, $\"api/admin/cluster/primary/transferConfig?token={_token}&includeZones={(includeZones is null ? \"\" : includeZones.Join(','))}\");\n            httpRequest.Headers.IfModifiedSince = ifModifiedSince;\n\n            HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n\n            return (httpResponse.Content.ReadAsStream(cancellationToken), httpResponse.Content.Headers.LastModified?.UtcDateTime ?? DateTime.UtcNow);\n        }\n\n        public async Task<ClusterInfo> LeaveClusterAsync(bool forceLeave = false, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            Stream stream = await _httpClient.GetStreamAsync($\"api/admin/cluster/secondary/leave?token={_token}&forceLeave={forceLeave}\", cancellationToken);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n\n            ClusterInfo? clusterInfo = rootElement.GetProperty(\"response\").Deserialize<ClusterInfo>(_serializerOptions);\n            if (clusterInfo is null)\n                throw new HttpApiClientException(\"Invalid JSON response was received.\");\n\n            return clusterInfo;\n        }\n\n        public async Task NotifySecondaryNodeAsync(int primaryNodeId, Uri primaryNodeUrl, IReadOnlyCollection<IPAddress> primaryNodeIpAddresses, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            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);\n\n            using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);\n            JsonElement rootElement = jsonDoc.RootElement;\n\n            CheckResponseStatus(rootElement);\n        }\n\n        public async Task ProxyRequest(HttpContext context, string actingUsername, CancellationToken cancellationToken = default)\n        {\n            if (!_loggedIn)\n                throw new HttpApiClientException(\"No active session exists. Please login and try again.\");\n\n            //read input http request and send http response to node\n            HttpRequest inHttpRequest = context.Request;\n\n            StringBuilder queryString = new StringBuilder();\n\n            queryString.Append(\"?actingUser=\").Append(HttpUtility.UrlEncode(actingUsername));\n\n            foreach (KeyValuePair<string, StringValues> query in inHttpRequest.Query)\n            {\n                string key = query.Key;\n                string value = query.Value.ToString();\n\n                switch (key)\n                {\n                    case \"token\":\n                        //use http client token\n                        value = _token!;\n                        break;\n\n                    case \"node\":\n                        //skip node name\n                        continue;\n                }\n\n                queryString.Append('&').Append(key).Append('=').Append(HttpUtility.UrlEncode(value));\n            }\n\n            HttpRequestMessage httpRequest = new HttpRequestMessage(new HttpMethod(inHttpRequest.Method), new Uri(_serverUrl, inHttpRequest.Path + queryString.ToString()));\n\n            if (inHttpRequest.HasFormContentType)\n            {\n                if (inHttpRequest.Form.Keys.Count > 0)\n                {\n                    Dictionary<string, string> formParams = new Dictionary<string, string>(inHttpRequest.Form.Count);\n\n                    foreach (KeyValuePair<string, StringValues> formParam in inHttpRequest.Form)\n                    {\n                        string key = formParam.Key;\n                        string value = formParam.Value.ToString();\n\n                        switch (key)\n                        {\n                            case \"token\":\n                                //use http client token\n                                value = _token!;\n                                break;\n\n                            case \"node\":\n                                //skip node name\n                                continue;\n                        }\n\n                        formParams[key] = value;\n                    }\n\n                    httpRequest.Content = new FormUrlEncodedContent(formParams);\n                }\n                else if (inHttpRequest.Form.Files.Count > 0)\n                {\n                    MultipartFormDataContent formData = new MultipartFormDataContent();\n\n                    foreach (IFormFile file in inHttpRequest.Form.Files)\n                        formData.Add(new StreamContent(file.OpenReadStream()), file.Name, file.FileName);\n\n                    httpRequest.Content = formData;\n                }\n                else\n                {\n                    throw new InvalidOperationException();\n                }\n            }\n            else\n            {\n                httpRequest.Content = new StreamContent(inHttpRequest.Body);\n            }\n\n            foreach (KeyValuePair<string, StringValues> inHeader in inHttpRequest.Headers)\n            {\n                if (!httpRequest.Headers.TryAddWithoutValidation(inHeader.Key, inHeader.Value.ToString()))\n                {\n                    if (!inHttpRequest.HasFormContentType)\n                    {\n                        //add content headers only when there is no form data\n                        if (!httpRequest.Content.Headers.TryAddWithoutValidation(inHeader.Key, inHeader.Value.ToString()))\n                            throw new InvalidOperationException();\n                    }\n                }\n            }\n\n            HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n\n            //receive http response and write to output http response\n            HttpResponse outHttpResponse = context.Response;\n\n            foreach (KeyValuePair<string, IEnumerable<string>> header in httpResponse.Headers)\n            {\n                if (header.Key.Equals(\"transfer-encoding\", StringComparison.OrdinalIgnoreCase) && (httpResponse.Headers.TransferEncodingChunked == true))\n                    continue; //skip chunked header to allow kestrel to do the chunking\n\n                if (!outHttpResponse.Headers.TryAdd(header.Key, header.Value.Join()))\n                    throw new InvalidOperationException();\n            }\n\n            foreach (KeyValuePair<string, IEnumerable<string>> header in httpResponse.Content.Headers)\n            {\n                if (header.Key.Equals(\"content-length\", StringComparison.OrdinalIgnoreCase) && (httpResponse.Headers.TransferEncodingChunked == true))\n                    continue; //skip content length when data is chunked\n\n                if (!outHttpResponse.Headers.TryAdd(header.Key, header.Value.Join()))\n                    throw new InvalidOperationException();\n            }\n\n            await httpResponse.Content.CopyToAsync(outHttpResponse.Body, cancellationToken);\n        }\n\n        #endregion\n\n        #region properties\n\n        public Uri ServerUrl\n        { get { return _serverUrl; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.HttpApi/HttpApiClientException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore.HttpApi\n{\n    public class HttpApiClientException : Exception\n    {\n        #region constructors\n\n        public HttpApiClientException()\n            : base()\n        { }\n\n        public HttpApiClientException(string message)\n            : base(message)\n        { }\n\n        public HttpApiClientException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.HttpApi/InvalidTokenHttpApiClientException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore.HttpApi\n{\n    public class InvalidTokenHttpApiClientException : HttpApiClientException\n    {\n        #region constructors\n\n        public InvalidTokenHttpApiClientException()\n            : base()\n        { }\n\n        public InvalidTokenHttpApiClientException(string message)\n            : base(message)\n        { }\n\n        public InvalidTokenHttpApiClientException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.HttpApi/Models/ClusterInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace DnsServerCore.HttpApi.Models\n{\n    public class ClusterInfo\n    {\n        public bool ClusterInitialized { get; set; }\n        public string? ClusterDomain { get; set; }\n        public ushort HeartbeatRefreshIntervalSeconds { get; set; }\n        public ushort HeartbeatRetryIntervalSeconds { get; set; }\n        public ushort ConfigRefreshIntervalSeconds { get; set; }\n        public ushort ConfigRetryIntervalSeconds { get; set; }\n        public DateTime? ConfigLastSynced { get; set; }\n        public List<ClusterNodeInfo>? ClusterNodes { get; set; }\n\n        public class ClusterNodeInfo\n        {\n            public int Id { get; set; }\n            public required string Name { get; set; }\n            public required Uri Url { get; set; }\n            public required string[] IPAddresses { get; set; }\n            public required string Type { get; set; }\n            public required string State { get; set; }\n            public DateTime? UpSince { get; set; }\n            public DateTime? LastSeen { get; set; }\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.HttpApi/Models/DashboardStats.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace DnsServerCore.HttpApi.Models\n{\n    public enum DashboardStatsType\n    {\n        Unknown = 0,\n        LastHour = 1,\n        LastDay = 2,\n        LastWeek = 3,\n        LastMonth = 4,\n        LastYear = 5,\n        Custom = 6\n    }\n\n    public enum DashboardTopStatsType\n    {\n        Unknown = 0,\n        TopClients = 1,\n        TopDomains = 2,\n        TopBlockedDomains = 3\n    }\n\n    public class DashboardStats\n    {\n        public StatsData? Stats { get; set; }\n        public ChartData? MainChartData { get; set; }\n        public ChartData? QueryResponseChartData { get; set; }\n        public ChartData? QueryTypeChartData { get; set; }\n        public ChartData? ProtocolTypeChartData { get; set; }\n        public TopClientStats[]? TopClients { get; set; }\n        public TopStats[]? TopDomains { get; set; }\n        public TopStats[]? TopBlockedDomains { get; set; }\n\n        public void Merge(DashboardStats other, int limit)\n        {\n            if ((Stats is not null) && (other.Stats is not null))\n                Stats.Merge(other.Stats);\n\n            if ((MainChartData is not null) && (other.MainChartData is not null))\n                MainChartData = ChartData.Merge(MainChartData, other.MainChartData, false);\n\n            if ((QueryResponseChartData is not null) && (other.QueryResponseChartData is not null))\n                QueryResponseChartData = ChartData.Merge(QueryResponseChartData, other.QueryResponseChartData, false);\n\n            if ((QueryTypeChartData is not null) && (other.QueryTypeChartData is not null))\n                QueryTypeChartData = ChartData.Merge(QueryTypeChartData, other.QueryTypeChartData, true);\n\n            if ((ProtocolTypeChartData is not null) && (other.ProtocolTypeChartData is not null))\n                ProtocolTypeChartData = ChartData.Merge(ProtocolTypeChartData, other.ProtocolTypeChartData, true);\n\n            if ((TopClients is not null) && (other.TopClients is not null))\n                TopClients = TopStats.Merge(TopClients, other.TopClients, limit);\n\n            if ((TopDomains is not null) && (other.TopDomains is not null))\n                TopDomains = TopStats.Merge(TopDomains, other.TopDomains, limit);\n\n            if ((TopBlockedDomains is not null) && (other.TopBlockedDomains is not null))\n                TopBlockedDomains = TopStats.Merge(TopBlockedDomains, other.TopBlockedDomains, limit);\n        }\n\n        public class StatsData\n        {\n            public long TotalQueries { get; set; }\n            public long TotalNoError { get; set; }\n            public long TotalServerFailure { get; set; }\n            public long TotalNxDomain { get; set; }\n            public long TotalRefused { get; set; }\n            public long TotalAuthoritative { get; set; }\n            public long TotalRecursive { get; set; }\n            public long TotalCached { get; set; }\n            public long TotalBlocked { get; set; }\n            public long TotalDropped { get; set; }\n            public long TotalClients { get; set; }\n            public int Zones { get; set; }\n            public long CachedEntries { get; set; }\n            public int AllowedZones { get; set; }\n            public int BlockedZones { get; set; }\n            public int AllowListZones { get; set; }\n            public int BlockListZones { get; set; }\n\n            public void Merge(StatsData statsData)\n            {\n                TotalQueries += statsData.TotalQueries;\n                TotalNoError += statsData.TotalNoError;\n                TotalServerFailure += statsData.TotalServerFailure;\n                TotalNxDomain += statsData.TotalNxDomain;\n                TotalRefused += statsData.TotalRefused;\n\n                TotalAuthoritative += statsData.TotalAuthoritative;\n                TotalRecursive += statsData.TotalRecursive;\n                TotalCached += statsData.TotalCached;\n                TotalBlocked += statsData.TotalBlocked;\n                TotalDropped += statsData.TotalDropped;\n\n                if (statsData.TotalClients > TotalClients)\n                    TotalClients = statsData.TotalClients;\n\n                if (statsData.Zones > Zones)\n                    Zones = statsData.Zones;\n\n                if (statsData.CachedEntries > CachedEntries)\n                    CachedEntries = statsData.CachedEntries;\n\n                if (statsData.AllowedZones > AllowedZones)\n                    AllowedZones = statsData.AllowedZones;\n\n                if (statsData.BlockedZones > BlockedZones)\n                    BlockedZones = statsData.BlockedZones;\n\n                if (statsData.AllowListZones > AllowListZones)\n                    AllowListZones = statsData.AllowListZones;\n\n                if (statsData.BlockListZones > BlockListZones)\n                    BlockListZones = statsData.BlockListZones;\n            }\n        }\n\n        public class ChartData\n        {\n            public required string[] Labels { get; set; }\n            public required DataSet[] DataSets { get; set; }\n\n            internal static ChartData Merge(ChartData x, ChartData y, bool sortByData)\n            {\n                Dictionary<string, Dictionary<string, long>> aggregateDataSet = new Dictionary<string, Dictionary<string, long>>(x.Labels.Length + y.Labels.Length);\n\n                foreach (DataSet dataSet in x.DataSets)\n                {\n                    Dictionary<string, long> data = new Dictionary<string, long>(dataSet.Data.Length);\n\n                    for (int i = 0; i < dataSet.Data.Length; i++)\n                        data[x.Labels[i]] = dataSet.Data[i];\n\n                    aggregateDataSet[dataSet.Label ?? \"\"] = data;\n                }\n\n                foreach (DataSet dataSet in y.DataSets)\n                {\n                    if (!aggregateDataSet.TryGetValue(dataSet.Label ?? \"\", out Dictionary<string, long>? data))\n                    {\n                        data = new Dictionary<string, long>(dataSet.Data.Length);\n                        aggregateDataSet[dataSet.Label ?? \"\"] = data;\n                    }\n\n                    for (int i = 0; i < dataSet.Data.Length; i++)\n                    {\n                        string label = y.Labels[i];\n\n                        if (data.TryGetValue(label, out long value))\n                            data[label] = value + dataSet.Data[i];\n                        else\n                            data[label] = dataSet.Data[i];\n                    }\n                }\n\n                if (sortByData && (aggregateDataSet.Count == 1))\n                {\n                    //prepare single dataset with sorted data\n                    KeyValuePair<string, Dictionary<string, long>> firstDataSet = aggregateDataSet.First();\n                    Dictionary<string, long> dataSet = firstDataSet.Value;\n                    List<KeyValuePair<string, long>> sortedData = [.. dataSet];\n\n                    sortedData.Sort(delegate (KeyValuePair<string, long> item1, KeyValuePair<string, long> item2)\n                    {\n                        return item2.Value.CompareTo(item1.Value);\n                    });\n\n                    string[] labels = new string[sortedData.Count];\n                    long[] data = new long[sortedData.Count];\n\n                    for (int i = 0; i < sortedData.Count; i++)\n                    {\n                        labels[i] = sortedData[i].Key;\n                        data[i] = sortedData[i].Value;\n                    }\n\n                    return new ChartData\n                    {\n                        Labels = labels,\n                        DataSets =\n                        [\n                            new DataSet\n                            {\n                                Label = firstDataSet.Key == \"\" ? null : aggregateDataSet.First().Key,\n                                Data = data\n                            }\n                        ]\n                    };\n                }\n                else\n                {\n                    //prepare merged labels\n                    List<string> mergedLabels = new List<string>(x.Labels.Length + y.Labels.Length);\n\n                    mergedLabels.AddRange(x.Labels);\n\n                    foreach (string label in y.Labels)\n                    {\n                        if (!mergedLabels.Contains(label))\n                            mergedLabels.Add(label);\n                    }\n\n                    //prepare merged datasets with ordered data\n                    List<DataSet> mergedDataSets = new List<DataSet>(aggregateDataSet.Count);\n\n                    foreach (KeyValuePair<string, Dictionary<string, long>> dataSetEntry in aggregateDataSet)\n                    {\n                        long[] data = new long[mergedLabels.Count];\n\n                        for (int i = 0; i < mergedLabels.Count; i++)\n                        {\n                            string label = mergedLabels[i];\n\n                            if (dataSetEntry.Value.TryGetValue(label, out long value))\n                                data[i] = value;\n                        }\n\n                        mergedDataSets.Add(new DataSet\n                        {\n                            Label = dataSetEntry.Key == \"\" ? null : dataSetEntry.Key,\n                            Data = data\n                        });\n                    }\n\n                    return new ChartData\n                    {\n                        Labels = [.. mergedLabels],\n                        DataSets = [.. mergedDataSets]\n                    };\n                }\n            }\n\n            public void Trim(int limit)\n            {\n                if (Labels.Length > limit)\n                {\n                    string[] newLabels = new string[limit];\n\n                    for (int i = 0; i < limit - 1; i++)\n                        newLabels[i] = Labels[i];\n\n                    newLabels[limit - 1] = \"Others\";\n\n                    Labels = newLabels;\n\n                    foreach (DataSet dataSet in DataSets)\n                        dataSet.Trim(limit);\n                }\n            }\n        }\n\n        public class DataSet\n        {\n            public string? Label { get; set; }\n            public required long[] Data { get; set; }\n\n            public void Trim(int limit)\n            {\n                if (Data.Length > limit)\n                {\n                    long[] newData = new long[limit];\n\n                    for (int i = 0; i < newData.Length - 1; i++)\n                        newData[i] = Data[i];\n\n                    long othersCount = 0;\n\n                    for (int i = limit; i < Data.Length; i++)\n                        othersCount += Data[i];\n\n                    newData[limit - 1] = othersCount;\n\n                    Data = newData;\n                }\n            }\n        }\n\n        public class TopStats\n        {\n            public required string Name { get; set; }\n            public required long Hits { get; set; }\n\n            private static List<KeyValuePair<string, T>> GetTopList<T>(List<KeyValuePair<string, T>> list, int limit) where T : TopStats\n            {\n                list.Sort(delegate (KeyValuePair<string, T> item1, KeyValuePair<string, T> item2)\n                {\n                    return item2.Value.Hits.CompareTo(item1.Value.Hits);\n                });\n\n                if (list.Count > limit)\n                    list.RemoveRange(limit, list.Count - limit);\n\n                return list;\n            }\n\n            internal static T[] Merge<T>(T[] x, T[] y, int limit) where T : TopStats\n            {\n                Dictionary<string, T> aggregateData = new Dictionary<string, T>(x.Length + y.Length);\n\n                foreach (T item in x)\n                    aggregateData[item.Name] = item;\n\n                foreach (T item in y)\n                {\n                    if (aggregateData.TryGetValue(item.Name, out T? entry))\n                    {\n                        entry.Hits += item.Hits;\n\n                        if ((entry is TopClientStats topClientEntry) && (item is TopClientStats topClientItem))\n                        {\n                            topClientEntry.Domain ??= topClientItem.Domain;\n                            topClientEntry.RateLimited |= topClientItem.RateLimited;\n                        }\n                    }\n                    else\n                    {\n                        aggregateData[item.Name] = item;\n                    }\n                }\n\n                List<KeyValuePair<string, T>> topList = GetTopList([.. aggregateData], limit);\n\n                T[] z = new T[topList.Count];\n\n                for (int i = 0; i < topList.Count; i++)\n                    z[i] = topList[i].Value;\n\n                return z;\n            }\n        }\n\n        public class TopClientStats : TopStats\n        {\n            public string? Domain { get; set; }\n            public bool RateLimited { get; set; }\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.HttpApi/Models/SessionInfo.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Collections.Generic;\n\nnamespace DnsServerCore.HttpApi.Models\n{\n    public class SessionInfo\n    {\n        public string? DisplayName { get; set; }\n        public required string Username { get; set; }\n        public bool? TotpEnabled { get; set; }\n        public string? TokenName { get; set; }\n        public required string Token { get; set; }\n        public DetailedInfo? Info { get; set; }\n\n        public class DetailedInfo\n        {\n            public required string Version { get; set; }\n            public required string UpTimeStamp { get; set; }\n            public required string DnsServerDomain { get; set; }\n            public required int DefaultRecordTtl { get; set; }\n            public required bool UseSoaSerialDateScheme { get; set; }\n            public required bool DnssecValidation { get; set; }\n            public required Dictionary<string, PermissionInfo> Permissions { get; set; }\n        }\n\n        public class PermissionInfo\n        {\n            public required bool CanView { get; set; }\n            public required bool CanModify { get; set; }\n            public required bool CanDelete { get; set; }\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerCore.HttpApi/TwoFactorAuthRequiredHttpApiClientException.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\n\nnamespace DnsServerCore.HttpApi\n{\n    public class TwoFactorAuthRequiredHttpApiClientException : InvalidTokenHttpApiClientException\n    {\n        #region constructors\n\n        public TwoFactorAuthRequiredHttpApiClientException()\n            : base()\n        { }\n\n        public TwoFactorAuthRequiredHttpApiClientException(string message)\n            : base(message)\n        { }\n\n        public TwoFactorAuthRequiredHttpApiClientException(string message, Exception innerException)\n            : base(message, innerException)\n        { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/DnsProvider.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2023  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing TechnitiumLibrary.IO;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerSystemTrayApp\n{\n    public class DnsProvider : IComparable<DnsProvider>\n    {\n        #region variables\n\n        public string Name;\n        public ICollection<IPAddress> Addresses;\n\n        #endregion\n\n        #region constructor\n\n        public DnsProvider(string name, ICollection<IPAddress> addresses)\n        {\n            this.Name = name;\n            this.Addresses = addresses;\n        }\n\n        public DnsProvider(BinaryReader bR)\n        {\n            this.Name = bR.ReadShortString();\n            this.Addresses = new List<IPAddress>();\n\n            int count = bR.ReadInt32();\n\n            for (int i = 0; i < count; i++)\n                this.Addresses.Add(IPAddressExtensions.ReadFrom(bR));\n        }\n\n        #endregion\n\n        #region static\n\n        public static DnsProvider[] GetDefaultProviders()\n        {\n            return new DnsProvider[] {\n                new DnsProvider(\"Technitium\", new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }),\n                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]\") }),\n                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]\") }),\n                new DnsProvider(\"Quad9\", new IPAddress[] { IPAddress.Parse(\"9.9.9.9\"), IPAddress.Parse(\"[2620:fe::fe]\") }),\n                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]\") })\n            };\n        }\n\n        #endregion\n\n        #region public\n\n        public string GetIpv4Addresses()\n        {\n            string ipv4Addresses = null;\n\n            foreach (IPAddress address in Addresses)\n            {\n                if (address.AddressFamily == AddressFamily.InterNetwork)\n                {\n                    if (ipv4Addresses == null)\n                        ipv4Addresses = address.ToString();\n                    else\n                        ipv4Addresses += \", \" + address.ToString();\n                }\n            }\n\n            return ipv4Addresses;\n        }\n\n        public string GetIpv6Addresses()\n        {\n            string ipv6Addresses = null;\n\n            foreach (IPAddress address in Addresses)\n            {\n                if (address.AddressFamily == AddressFamily.InterNetworkV6)\n                {\n                    if (ipv6Addresses == null)\n                        ipv6Addresses = address.ToString();\n                    else\n                        ipv6Addresses += \", \" + address.ToString();\n                }\n            }\n\n            return ipv6Addresses;\n        }\n\n        public override string ToString()\n        {\n            return Name;\n        }\n\n        public int CompareTo(DnsProvider other)\n        {\n            return this.Name.CompareTo(other.Name);\n        }\n\n        public void WriteTo(BinaryWriter bW)\n        {\n            bW.WriteShortString(Name);\n\n            bW.Write(Addresses.Count);\n\n            foreach (IPAddress address in Addresses)\n                address.WriteTo(bW);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/DnsServerSystemTrayApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<OutputType>WinExe</OutputType>\n\t\t<TargetFramework>net9.0-windows7.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<UseWindowsForms>true</UseWindowsForms>\n\t\t<GenerateAssemblyInfo>true</GenerateAssemblyInfo>\n\t\t<RootNamespace>DnsServerSystemTrayApp</RootNamespace>\n\t\t<AssemblyName>DnsServerSystemTrayApp</AssemblyName>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<ApplicationIcon>logo2.ico</ApplicationIcon>\n\t\t<Version>6.1</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Description></Description>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary.IO\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.IO.dll</HintPath>\n\t\t</Reference>\n\t\t<Reference Include=\"TechnitiumLibrary.Net\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.dll</HintPath>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Compile Update=\"Properties\\Resources.Designer.cs\">\n\t\t\t<AutoGen>True</AutoGen>\n\t\t\t<DependentUpon>Resources.resx</DependentUpon>\n\t\t\t<DesignTime>True</DesignTime>\n\t\t</Compile>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Content Include=\"logo2.ico\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"System.Management\" Version=\"9.0.10\" />\n\t\t<PackageReference Include=\"System.ServiceProcess.ServiceController\" Version=\"9.0.10\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<EmbeddedResource Update=\"Properties\\Resources.resx\">\n\t\t\t<Generator>ResXFileCodeGenerator</Generator>\n\t\t\t<LastGenOutput>Resources.Designer.cs</LastGenOutput>\n\t\t</EmbeddedResource>\n\t</ItemGroup>\n</Project>"
  },
  {
    "path": "DnsServerSystemTrayApp/MainApplicationContext.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerSystemTrayApp.Properties;\nusing Microsoft.Win32;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Management;\nusing System.Net;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.ServiceProcess;\nusing System.Text;\nusing System.Windows.Forms;\nusing TechnitiumLibrary.Net;\n\nnamespace DnsServerSystemTrayApp\n{\n    public class MainApplicationContext : ApplicationContext\n    {\n        #region variables\n\n        const int SERVICE_WAIT_TIMEOUT_SECONDS = 30;\n        private readonly ServiceController _service = new ServiceController(\"DnsService\");\n\n        readonly string _configFile;\n        readonly List<DnsProvider> _dnsProviders = new List<DnsProvider>();\n\n        private NotifyIcon TrayIcon;\n        private ContextMenuStrip TrayIconContextMenu;\n        private ToolStripMenuItem DashboardMenuItem;\n        private ToolStripMenuItem NetworkDnsMenuItem;\n        private ToolStripMenuItem DefaultNetworkDnsMenuItem;\n        private ToolStripMenuItem ManageNetworkDnsMenuItem;\n        private ToolStripMenuItem ServiceMenuItem;\n        private ToolStripMenuItem StartServiceMenuItem;\n        private ToolStripMenuItem RestartServiceMenuItem;\n        private ToolStripMenuItem StopServiceMenuItem;\n        private ToolStripMenuItem FirewallMenuItem;\n        private ToolStripMenuItem AboutMenuItem;\n        private ToolStripMenuItem AutoStartMenuItem;\n        private ToolStripMenuItem ExitMenuItem;\n\n        #endregion\n\n        #region constructor\n\n        public MainApplicationContext(string configFile, string[] args, ref bool exitApp)\n        {\n            _configFile = configFile;\n            LoadConfig();\n\n            InitializeComponent();\n\n            if (args.Length > 0)\n            {\n                switch (args[0])\n                {\n                    case \"--network-dns-default-exit\":\n                        SetNetworkDnsToDefault(true);\n                        exitApp = true;\n                        break;\n\n                    case \"--network-dns-default\":\n                        SetNetworkDnsToDefault();\n                        break;\n\n                    case \"--network-dns-item\":\n                        foreach (DnsProvider dnsProvider in _dnsProviders)\n                        {\n                            if ((args.Length > 1) && dnsProvider.Name.Equals(args[1]))\n                            {\n                                NetworkDnsMenuSubItem_Click(new ToolStripMenuItem(dnsProvider.Name) { Tag = dnsProvider }, EventArgs.Empty);\n                                break;\n                            }\n                        }\n                        break;\n\n                    case \"--network-dns-manage\":\n                        ManageNetworkDnsMenuItem_Click(this, EventArgs.Empty);\n                        break;\n\n                    case \"--service-start\":\n                        StartServiceMenuItem_Click(this, EventArgs.Empty);\n                        break;\n\n                    case \"--service-restart\":\n                        RestartServiceMenuItem_Click(this, EventArgs.Empty);\n                        break;\n\n                    case \"--service-stop\":\n                        StopServiceMenuItem_Click(this, EventArgs.Empty);\n                        break;\n\n                    case \"--auto-firewall-entry\":\n                        if (args.Length > 1)\n                            SetAutoFirewallEntry(bool.Parse(args[1]));\n\n                        break;\n\n                    case \"--first-run\":\n                        bool usingLoopbackAsDns = false;\n\n                        try\n                        {\n                            foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())\n                            {\n                                if (nic.OperationalStatus != OperationalStatus.Up)\n                                    continue;\n\n                                foreach (IPAddress dnsAddress in nic.GetIPProperties().DnsAddresses)\n                                {\n                                    if (IPAddress.IsLoopback(dnsAddress))\n                                    {\n                                        usingLoopbackAsDns = true;\n                                        break;\n                                    }\n                                }\n\n                                if (usingLoopbackAsDns)\n                                    break;\n                            }\n                        }\n                        catch\n                        { }\n\n                        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)\n                            SetNetworkDns(new DnsProvider(\"Technitium\", new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }));\n\n                        break;\n                }\n            }\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        protected override void Dispose(bool disposing)\n        {\n            if (disposing)\n            {\n                TrayIcon?.Dispose();\n            }\n\n            base.Dispose(disposing);\n        }\n\n        #endregion\n\n        #region private\n\n        private void InitializeComponent()\n        {\n            //\n            // TrayIconContextMenu\n            //\n            TrayIconContextMenu = new ContextMenuStrip();\n            TrayIconContextMenu.SuspendLayout();\n\n            //\n            // TrayIcon\n            //\n            TrayIcon = new NotifyIcon();\n            TrayIcon.Icon = Resources.logo2;\n            TrayIcon.Visible = true;\n            TrayIcon.MouseUp += TrayIcon_MouseUp;\n            TrayIcon.ContextMenuStrip = TrayIconContextMenu;\n            TrayIcon.Text = Resources.ServiceName;\n\n            //\n            // DashboardMenuItem\n            //\n            DashboardMenuItem = new ToolStripMenuItem();\n            DashboardMenuItem.Name = \"DashboardMenuItem\";\n            DashboardMenuItem.Text = Resources.DashboardMenuItem;\n            DashboardMenuItem.Click += DashboardMenuItem_Click;\n\n\n            //\n            // NetworkDnsMenuItem\n            //\n            NetworkDnsMenuItem = new ToolStripMenuItem();\n            NetworkDnsMenuItem.Name = \"NetworkDnsMenuItem\";\n            NetworkDnsMenuItem.Text = Resources.NetworkDnsMenuItem;\n\n            DefaultNetworkDnsMenuItem = new ToolStripMenuItem(\"Default\");\n            DefaultNetworkDnsMenuItem.Click += DefaultNetworkDnsMenuItem_Click;\n\n            ManageNetworkDnsMenuItem = new ToolStripMenuItem(\"Manage\");\n            ManageNetworkDnsMenuItem.Click += ManageNetworkDnsMenuItem_Click;\n\n            //\n            // ServiceMenuItem\n            //\n            ServiceMenuItem = new ToolStripMenuItem();\n            ServiceMenuItem.Name = \"ServiceMenuItem\";\n            ServiceMenuItem.Text = Resources.ServiceMenuItem;\n\n            StartServiceMenuItem = new ToolStripMenuItem(Resources.ServiceStartMenuItem);\n            StartServiceMenuItem.Click += StartServiceMenuItem_Click;\n\n            RestartServiceMenuItem = new ToolStripMenuItem(Resources.ServiceRestartMenuItem);\n            RestartServiceMenuItem.Click += RestartServiceMenuItem_Click;\n\n            StopServiceMenuItem = new ToolStripMenuItem(Resources.ServiceStopMenuItem);\n            StopServiceMenuItem.Click += StopServiceMenuItem_Click;\n\n            ServiceMenuItem.DropDownItems.AddRange(new ToolStripItem[]\n            {\n                StartServiceMenuItem,\n                RestartServiceMenuItem,\n                StopServiceMenuItem\n            });\n\n            //\n            // FirewallMenuItem\n            //\n            FirewallMenuItem = new ToolStripMenuItem();\n            FirewallMenuItem.Name = \"FirewallMenuItem\";\n            FirewallMenuItem.Text = \"Auto &Firewall Entry\";\n            FirewallMenuItem.Click += FirewallMenuItem_Click;\n\n            //\n            // AboutMenuItem\n            //\n            AboutMenuItem = new ToolStripMenuItem();\n            AboutMenuItem.Name = \"AboutMenuItem\";\n            AboutMenuItem.Text = Resources.AboutMenuItem;\n            AboutMenuItem.Click += AboutMenuItem_Click;\n\n            //\n            // AutoStartMenuItem\n            //\n            AutoStartMenuItem = new ToolStripMenuItem();\n            AutoStartMenuItem.Name = \"AutoStartMenuItem\";\n            AutoStartMenuItem.Text = \"&Auto Start Icon\";\n            AutoStartMenuItem.Click += AutoStartMenuItem_Click;\n\n            //\n            // ExitMenuItem\n            //\n            ExitMenuItem = new ToolStripMenuItem();\n            ExitMenuItem.Name = \"ExitMenuItem\";\n            ExitMenuItem.Text = Resources.ExitMenuItem;\n            ExitMenuItem.Click += ExitMenuItem_Click;\n\n            TrayIconContextMenu.Items.AddRange(new ToolStripItem[]\n            {\n                DashboardMenuItem,\n                new ToolStripSeparator(),\n                NetworkDnsMenuItem,\n                ServiceMenuItem,\n                FirewallMenuItem,\n                AboutMenuItem,\n                new ToolStripSeparator(),\n                AutoStartMenuItem,\n                ExitMenuItem\n            });\n\n            TrayIconContextMenu.ResumeLayout(false);\n        }\n\n        private void LoadConfig()\n        {\n            try\n            {\n                using (FileStream fS = new FileStream(_configFile, FileMode.Open, FileAccess.Read))\n                {\n                    BinaryReader bR = new BinaryReader(fS);\n\n                    if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"DT\")\n                        throw new InvalidDataException(\"Invalid DNS Server System Tray App config file format.\");\n\n                    switch (bR.ReadByte())\n                    {\n                        case 1:\n                            int count = bR.ReadInt32();\n                            _dnsProviders.Clear();\n\n                            for (int i = 0; i < count; i++)\n                                _dnsProviders.Add(new DnsProvider(bR));\n\n                            _dnsProviders.Sort();\n                            break;\n\n                        default:\n                            throw new NotSupportedException(\"DNS Server System Tray App config file format is not supported.\");\n                    }\n                }\n            }\n            catch (FileNotFoundException)\n            {\n                _dnsProviders.Clear();\n                _dnsProviders.AddRange(DnsProvider.GetDefaultProviders());\n                _dnsProviders.Sort();\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error occurred while loading config file. \" + ex.Message, \"Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private void SaveConfig()\n        {\n            try\n            {\n                using (FileStream fS = new FileStream(_configFile, FileMode.Create, FileAccess.Write))\n                {\n                    BinaryWriter bW = new BinaryWriter(fS);\n\n                    bW.Write(Encoding.ASCII.GetBytes(\"DT\"));\n                    bW.Write((byte)1);\n\n                    bW.Write(_dnsProviders.Count);\n\n                    foreach (DnsProvider dnsProvider in _dnsProviders)\n                        dnsProvider.WriteTo(bW);\n                }\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error occurred while saving config file. \" + ex.Message, \"Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private static void SetNetworkDns(DnsProvider dnsProvider)\n        {\n            if (!Program.IsAdmin)\n            {\n                Program.RunAsAdmin(\"--network-dns-item \" + dnsProvider.Name);\n                return;\n            }\n\n            try\n            {\n                foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())\n                {\n                    if (nic.OperationalStatus != OperationalStatus.Up)\n                        continue;\n\n                    IPInterfaceProperties properties = nic.GetIPProperties();\n\n                    if ((properties.DnsAddresses.Count > 0) && !properties.DnsAddresses[0].IsIPv6SiteLocal)\n                        SetNameServer(nic, dnsProvider.Addresses);\n                }\n\n                MessageBox.Show(\"The network DNS servers were set to \" + dnsProvider.Name + \" successfully.\", dnsProvider.Name + \" Configured - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information);\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error occurred while setting \" + dnsProvider.Name + \" as network DNS server. \" + ex.Message, \"Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private static void SetNameServer(NetworkInterface nic, ICollection<IPAddress> dnsAddresses)\n        {\n            SetNameServerIPv4(nic, dnsAddresses);\n            SetNameServerIPv6(nic, dnsAddresses);\n        }\n\n        private static void SetNameServerIPv4(NetworkInterface nic, ICollection<IPAddress> dnsAddresses)\n        {\n            ManagementClass networkAdapterConfig = new ManagementClass(\"Win32_NetworkAdapterConfiguration\");\n            ManagementObjectCollection instances = networkAdapterConfig.GetInstances();\n\n            foreach (ManagementObject obj in instances)\n            {\n                if ((bool)obj[\"IPEnabled\"] && obj[\"SettingID\"].Equals(nic.Id))\n                {\n                    List<string> dnsServers = new List<string>();\n\n                    foreach (IPAddress dnsAddress in dnsAddresses)\n                    {\n                        if (dnsAddress.AddressFamily != AddressFamily.InterNetwork)\n                            continue;\n\n                        dnsServers.Add(dnsAddress.ToString());\n                    }\n\n                    ManagementBaseObject objParameter = obj.GetMethodParameters(\"SetDNSServerSearchOrder\");\n                    objParameter[\"DNSServerSearchOrder\"] = dnsServers.ToArray();\n\n                    ManagementBaseObject response = obj.InvokeMethod(\"SetDNSServerSearchOrder\", objParameter, null);\n                    uint returnValue = (uint)response.GetPropertyValue(\"ReturnValue\");\n\n                    switch (returnValue)\n                    {\n                        case 0: //success\n                        case 1: //reboot required\n                            break;\n\n                        case 64:\n                            throw new Exception(\"Method not supported on this platform. WMI error code: \" + returnValue);\n\n                        case 65:\n                            throw new Exception(\"Unknown failure. WMI error code: \" + returnValue);\n\n                        case 70:\n                            throw new Exception(\"Invalid IP address. WMI error code: \" + returnValue);\n\n                        case 96:\n                            throw new Exception(\"Unable to notify DNS service. WMI error code: \" + returnValue);\n\n                        case 97:\n                            throw new Exception(\"Interface not configurable. WMI error code: \" + returnValue);\n\n                        default:\n                            throw new Exception(\"WMI error code: \" + returnValue);\n                    }\n\n                    break;\n                }\n            }\n        }\n\n        private static void SetNameServerIPv6(NetworkInterface nic, ICollection<IPAddress> dnsAddresses)\n        {\n            //HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces\\{}\n\n            string nameServer = null;\n\n            foreach (IPAddress dnsAddress in dnsAddresses)\n            {\n                if (dnsAddress.AddressFamily != AddressFamily.InterNetworkV6)\n                    continue;\n\n                if (nameServer == null)\n                    nameServer = dnsAddress.ToString();\n                else\n                    nameServer += \",\" + dnsAddress.ToString();\n            }\n\n            if (nameServer == null)\n                nameServer = \"\";\n\n            using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@\"SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces\\\" + nic.Id, true))\n            {\n                if (key is not null)\n                    key.SetValue(\"NameServer\", nameServer, RegistryValueKind.String);\n            }\n        }\n\n        private static void SetAutoFirewallEntry(bool value)\n        {\n            try\n            {\n                using (RegistryKey key = Registry.LocalMachine.CreateSubKey(@\"SOFTWARE\\Technitium\\DNS Server\", true))\n                {\n                    if (key is not null)\n                        key.SetValue(\"AutoFirewallEntry\", value ? 1 : 0, RegistryValueKind.DWord);\n                }\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error occurred while setting auto firewall registry entry value. \" + ex.Message, \"Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private static bool AddressExists(ICollection<IPAddress> checkAddresses, ICollection<IPAddress> addresses)\n        {\n            foreach (IPAddress checkAddress in checkAddresses)\n            {\n                foreach (IPAddress address in addresses)\n                {\n                    if (checkAddress.Equals(address))\n                        return true;\n                }\n            }\n\n            return false;\n        }\n\n        private void TrayIcon_MouseUp(object sender, MouseEventArgs e)\n        {\n            if (e.Button == MouseButtons.Right)\n            {\n                #region Network DNS\n\n                List<IPAddress> networkDnsAddresses = new List<IPAddress>();\n\n                try\n                {\n                    foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())\n                    {\n                        if (nic.OperationalStatus != OperationalStatus.Up)\n                            continue;\n\n                        networkDnsAddresses.AddRange(nic.GetIPProperties().DnsAddresses);\n                    }\n                }\n                catch\n                { }\n\n                NetworkDnsMenuItem.DropDownItems.Clear();\n                NetworkDnsMenuItem.DropDownItems.Add(DefaultNetworkDnsMenuItem);\n                NetworkDnsMenuItem.DropDownItems.Add(new ToolStripSeparator());\n\n                bool noItemChecked = true;\n                DefaultNetworkDnsMenuItem.Checked = false;\n\n                foreach (DnsProvider dnsProvider in _dnsProviders)\n                {\n                    ToolStripMenuItem item = new ToolStripMenuItem(dnsProvider.Name);\n                    item.Tag = dnsProvider;\n                    item.Click += NetworkDnsMenuSubItem_Click;\n\n                    if (AddressExists(networkDnsAddresses, dnsProvider.Addresses))\n                    {\n                        item.Checked = true;\n                        noItemChecked = false;\n                    }\n\n                    NetworkDnsMenuItem.DropDownItems.Add(item);\n                }\n\n                if (noItemChecked)\n                {\n                    foreach (IPAddress dnsAddress in networkDnsAddresses)\n                    {\n                        if (!dnsAddress.IsIPv6SiteLocal)\n                        {\n                            DefaultNetworkDnsMenuItem.Checked = true;\n                            break;\n                        }\n                    }\n                }\n\n                if (_dnsProviders.Count > 0)\n                    NetworkDnsMenuItem.DropDownItems.Add(new ToolStripSeparator());\n\n                NetworkDnsMenuItem.DropDownItems.Add(ManageNetworkDnsMenuItem);\n\n                #endregion\n\n                #region service\n\n                try\n                {\n                    _service.Refresh();\n\n                    switch (_service.Status)\n                    {\n                        case ServiceControllerStatus.Stopped:\n                            DashboardMenuItem.Enabled = false;\n                            StartServiceMenuItem.Enabled = true;\n                            RestartServiceMenuItem.Enabled = false;\n                            StopServiceMenuItem.Enabled = false;\n                            break;\n\n                        case ServiceControllerStatus.Running:\n                            DashboardMenuItem.Enabled = true;\n                            StartServiceMenuItem.Enabled = false;\n                            RestartServiceMenuItem.Enabled = true;\n                            StopServiceMenuItem.Enabled = true;\n                            break;\n\n                        default:\n                            DashboardMenuItem.Enabled = false;\n                            StartServiceMenuItem.Enabled = false;\n                            RestartServiceMenuItem.Enabled = false;\n                            StopServiceMenuItem.Enabled = false;\n                            break;\n                    }\n\n                    ServiceMenuItem.Enabled = true;\n                }\n                catch\n                {\n                    DashboardMenuItem.Enabled = false;\n                    ServiceMenuItem.Enabled = false;\n                }\n\n                #endregion\n\n                #region auto firewall\n\n                bool autoFirewallEntry = true;\n\n                try\n                {\n                    using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@\"SOFTWARE\\Technitium\\DNS Server\", false))\n                    {\n                        if (key is not null)\n                            autoFirewallEntry = Convert.ToInt32(key.GetValue(\"AutoFirewallEntry\", 1)) == 1;\n                    }\n                }\n                catch\n                { }\n\n                FirewallMenuItem.Checked = autoFirewallEntry;\n\n                #endregion\n\n                #region auto start\n\n                try\n                {\n                    using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@\"Software\\Microsoft\\Windows\\CurrentVersion\\Run\", false))\n                    {\n                        if (key is not null)\n                        {\n                            string autoStartPath = key.GetValue(\"Technitium DNS System Tray\") as string;\n\n                            AutoStartMenuItem.Checked = (autoStartPath != null) && autoStartPath.Equals(\"\\\"\" + Program.APP_PATH + \"\\\"\");\n                        }\n                    }\n                }\n                catch\n                { }\n\n                #endregion\n\n                TrayIcon.ShowContextMenu();\n            }\n        }\n\n        private void DashboardMenuItem_Click(object sender, EventArgs e)\n        {\n            int port = 5380;\n            string host = \"localhost\";\n\n            //try finding port number from web service config file\n            try\n            {\n                string webServiceConfigFile = Path.Combine(Path.GetDirectoryName(Program.APP_PATH), \"config\", \"webservice.config\");\n\n                using (FileStream fS = new FileStream(webServiceConfigFile, FileMode.Open, FileAccess.Read))\n                {\n                    BinaryReader bR = new BinaryReader(fS);\n\n                    if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != \"WC\") //format\n                        throw new InvalidDataException(\"DNS Server config file format is invalid.\");\n\n                    int version = bR.ReadByte();\n                    if (version > 0)\n                    {\n                        port = bR.ReadInt32(); //http port\n                        _ = bR.ReadInt32(); //https port\n\n                        {\n                            int count = bR.ReadByte();\n                            if (count > 0)\n                            {\n                                IPAddress localAddress = IPAddressExtensions.ReadFrom(bR);\n\n                                if (!IPAddress.IPv6Any.Equals(localAddress) && !IPAddress.Any.Equals(localAddress) && !IPAddress.IsLoopback(localAddress))\n                                    host = localAddress.ToString();\n                            }\n                        }\n                    }\n                }\n            }\n            catch\n            { }\n\n            ProcessStartInfo processInfo = new ProcessStartInfo($\"http://{host}:{port}\");\n\n            processInfo.UseShellExecute = true;\n            processInfo.Verb = \"open\";\n\n            Process.Start(processInfo);\n        }\n\n        private void DefaultNetworkDnsMenuItem_Click(object sender, EventArgs e)\n        {\n            SetNetworkDnsToDefault();\n        }\n\n        private static void SetNetworkDnsToDefault(bool silent = false)\n        {\n            if (!Program.IsAdmin)\n            {\n                if (!silent)\n                    Program.RunAsAdmin(\"--network-dns-default\");\n\n                return;\n            }\n\n            try\n            {\n                foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())\n                {\n                    if (nic.OperationalStatus != OperationalStatus.Up)\n                        continue;\n\n                    SetNameServerIPv6(nic, Array.Empty<IPAddress>());\n\n                    try\n                    {\n                        IPInterfaceProperties properties = nic.GetIPProperties();\n\n                        if (properties.GetIPv4Properties().IsDhcpEnabled)\n                        {\n                            SetNameServerIPv4(nic, Array.Empty<IPAddress>());\n                        }\n                        else if (properties.GatewayAddresses.Count > 0)\n                        {\n                            SetNameServerIPv4(nic, new IPAddress[] { properties.GatewayAddresses[0].Address });\n                        }\n                        else\n                        {\n                            SetNameServerIPv4(nic, Array.Empty<IPAddress>());\n                        }\n                    }\n                    catch (NetworkInformationException)\n                    { }\n                }\n\n                if (!silent)\n                    MessageBox.Show(\"The network DNS servers were set to default successfully.\", \"Default DNS Set - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information);\n            }\n            catch (Exception ex)\n            {\n                if (!silent)\n                    MessageBox.Show(\"Error occurred while setting default network DNS servers. \" + ex.Message, \"Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private void ManageNetworkDnsMenuItem_Click(object sender, EventArgs e)\n        {\n            if (!Program.IsAdmin)\n            {\n                Program.RunAsAdmin(\"--network-dns-manage\");\n                return;\n            }\n\n            using (frmManageDnsProviders frm = new frmManageDnsProviders(_dnsProviders))\n            {\n                if (frm.ShowDialog() == DialogResult.OK)\n                {\n                    _dnsProviders.Clear();\n                    _dnsProviders.AddRange(frm.DnsProviders);\n                    _dnsProviders.Sort();\n\n                    SaveConfig();\n                }\n            }\n        }\n\n        private void NetworkDnsMenuSubItem_Click(object sender, EventArgs e)\n        {\n            ToolStripMenuItem item = sender as ToolStripMenuItem;\n            DnsProvider dnsProvider = item.Tag as DnsProvider;\n\n            SetNetworkDns(dnsProvider);\n        }\n\n        private void StartServiceMenuItem_Click(object sender, EventArgs e)\n        {\n            if (!Program.IsAdmin)\n            {\n                Program.RunAsAdmin(\"--service-start\");\n                return;\n            }\n\n            try\n            {\n                _service.Start();\n                _service.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS));\n\n                MessageBox.Show(\"The service was started successfully.\", \"Service Started - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information);\n            }\n            catch (System.ServiceProcess.TimeoutException ex)\n            {\n                MessageBox.Show(\"The service did not respond in time.\" + ex.Message, \"Service Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error occurred while starting service. \" + ex.Message, \"Service Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private void RestartServiceMenuItem_Click(object sender, EventArgs e)\n        {\n            if (!Program.IsAdmin)\n            {\n                Program.RunAsAdmin(\"--service-restart\");\n                return;\n            }\n\n            try\n            {\n                _service.Stop();\n                _service.WaitForStatus(ServiceControllerStatus.Stopped, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS));\n                _service.Start();\n                _service.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS));\n\n                MessageBox.Show(\"The service was restarted successfully.\", \"Service Restarted - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information);\n            }\n            catch (System.ServiceProcess.TimeoutException ex)\n            {\n                MessageBox.Show(\"The service did not respond in time.\" + ex.Message, \"Service Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error occurred while restarting service. \" + ex.Message, \"Service Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private void StopServiceMenuItem_Click(object sender, EventArgs e)\n        {\n            if (!Program.IsAdmin)\n            {\n                Program.RunAsAdmin(\"--service-stop\");\n                return;\n            }\n\n            try\n            {\n                _service.Stop();\n                _service.WaitForStatus(ServiceControllerStatus.Stopped, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS));\n\n                MessageBox.Show(\"The service was stopped successfully.\", \"Service Stopped - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information);\n            }\n            catch (System.ServiceProcess.TimeoutException ex)\n            {\n                MessageBox.Show(\"The service did not respond in time.\" + ex.Message, \"Service Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error occurred while stopping service. \" + ex.Message, \"Service Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n        }\n\n        private void FirewallMenuItem_Click(object sender, EventArgs e)\n        {\n            if (!Program.IsAdmin)\n            {\n                Program.RunAsAdmin(\"--auto-firewall-entry \" + (!FirewallMenuItem.Checked).ToString());\n                return;\n            }\n\n            SetAutoFirewallEntry(!FirewallMenuItem.Checked);\n        }\n\n        private void AboutMenuItem_Click(object sender, EventArgs e)\n        {\n            using (frmAbout frm = new frmAbout())\n            {\n                frm.ShowDialog();\n            }\n        }\n\n        private void AutoStartMenuItem_Click(object sender, EventArgs e)\n        {\n            if (AutoStartMenuItem.Checked)\n            {\n                //remove\n                try\n                {\n                    using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@\"Software\\Microsoft\\Windows\\CurrentVersion\\Run\", true))\n                    {\n                        if (key is not null)\n                            key.DeleteValue(\"Technitium DNS System Tray\", false);\n                    }\n                }\n                catch (Exception ex)\n                {\n                    MessageBox.Show(\"Error occurred while removing auto start registry entry. \" + ex.Message, \"Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n                }\n            }\n            else\n            {\n                //add\n                try\n                {\n                    using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@\"Software\\Microsoft\\Windows\\CurrentVersion\\Run\", true))\n                    {\n                        if (key is not null)\n                            key.SetValue(\"Technitium DNS System Tray\", \"\\\"\" + Program.APP_PATH + \"\\\"\", RegistryValueKind.String);\n                    }\n                }\n                catch (Exception ex)\n                {\n                    MessageBox.Show(\"Error occurred while adding auto start registry entry. \" + ex.Message, \"Error - \" + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error);\n                }\n            }\n        }\n\n        private void ExitMenuItem_Click(object sender, EventArgs e)\n        {\n            if (MessageBox.Show(Resources.AreYouSureYouWantToQuit, Resources.Quit + \" - \" + Resources.ServiceName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes)\n                Application.Exit();\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/NotifyIconExtension.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2019  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Reflection;\nusing System.Windows.Forms;\n\nnamespace DnsServerSystemTrayApp\n{\n    public static class NotifyIconExtension\n    {\n        public static void ShowContextMenu(this NotifyIcon notifyIcon)\n        {\n            MethodInfo methodInfo = typeof(NotifyIcon).GetMethod(\"ShowContextMenu\", BindingFlags.Instance | BindingFlags.NonPublic);\n            methodInfo.Invoke(notifyIcon, null);\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/Program.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2022  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Reflection;\nusing System.Security.Principal;\nusing System.Threading;\nusing System.Windows.Forms;\n\nnamespace DnsServerSystemTrayApp\n{\n    static class Program\n    {\n        #region variables\n\n        const string MUTEX_NAME = \"TechnitiumDnsServerSystemTrayApp\";\n\n        public static readonly string APP_PATH = Assembly.GetEntryAssembly().Location;\n\n        static readonly bool _isAdmin = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);\n        static Mutex _app;\n\n        #endregion\n\n        #region constructor\n\n        static Program()\n        {\n            if (APP_PATH.EndsWith(\".dll\", StringComparison.OrdinalIgnoreCase))\n                APP_PATH = APP_PATH.Substring(0, APP_PATH.Length - 4) + \".exe\";\n        }\n\n        #endregion\n\n        #region public\n\n        [STAThread]\n        public static void Main(string[] args)\n        {\n            Application.EnableVisualStyles();\n            Application.SetCompatibleTextRenderingDefault(false);\n\n            #region check for multiple instances\n\n            _app = new Mutex(true, MUTEX_NAME, out bool createdNewMutex);\n\n            bool exitApp = false;\n\n            if (!createdNewMutex)\n            {\n                if (args.Length == 0)\n                {\n                    MessageBox.Show(\"Technitium DNS Server system tray app is already running.\", \"Already Running!\", MessageBoxButtons.OK, MessageBoxIcon.Information);\n                    return;\n                }\n                else\n                {\n                    exitApp = true;\n                }\n            }\n\n            #endregion\n\n            string configFile = Path.Combine(Path.GetDirectoryName(APP_PATH), \"SystemTrayApp.config\");\n\n            MainApplicationContext mainApp = new MainApplicationContext(configFile, args, ref exitApp);\n\n            if (exitApp)\n                mainApp.Dispose();\n            else\n                Application.Run(mainApp);\n        }\n\n        public static void RunAsAdmin(string args)\n        {\n            if (_isAdmin)\n                throw new Exception(\"App is already running as admin.\");\n\n            ProcessStartInfo processInfo = new ProcessStartInfo(APP_PATH, args);\n\n            processInfo.UseShellExecute = true;\n            processInfo.Verb = \"runas\";\n\n            try\n            {\n                _app.Dispose();\n                Process.Start(processInfo);\n                Application.Exit();\n                return;\n            }\n            catch (Exception ex)\n            {\n                MessageBox.Show(\"Error! \" + ex.Message, \"Error!\", MessageBoxButtons.OK, MessageBoxIcon.Error);\n            }\n\n            //user cancels UAC or exception occurred\n            _app = new Mutex(true, MUTEX_NAME, out _);\n        }\n\n        #endregion\n\n        #region properties\n\n        public static bool IsAdmin\n        { get { return _isAdmin; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/Properties/PublishProfiles/FolderProfile.pubxml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->\n<Project>\n  <PropertyGroup>\n    <Configuration>Release</Configuration>\n    <Platform>Any CPU</Platform>\n    <PublishDir>..\\DnsServerWindowsSetup\\publish</PublishDir>\n    <PublishProtocol>FileSystem</PublishProtocol>\n    <_TargetId>Folder</_TargetId>\n    <TargetFramework>net9.0-windows7.0</TargetFramework>\n    <SelfContained>false</SelfContained>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "DnsServerSystemTrayApp/Properties/Resources.Designer.cs",
    "content": "﻿//------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//     Runtime Version:4.0.30319.42000\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n//------------------------------------------------------------------------------\n\nnamespace DnsServerSystemTrayApp.Properties {\n    using System;\n    \n    \n    /// <summary>\n    ///   A strongly-typed resource class, for looking up localized strings, etc.\n    /// </summary>\n    // This class was auto-generated by the StronglyTypedResourceBuilder\n    // class via a tool like ResGen or Visual Studio.\n    // To add or remove a member, edit your .ResX file then rerun ResGen\n    // with the /str option, or rebuild your VS project.\n    [global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"System.Resources.Tools.StronglyTypedResourceBuilder\", \"16.0.0.0\")]\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]\n    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]\n    internal class Resources {\n        \n        private static global::System.Resources.ResourceManager resourceMan;\n        \n        private static global::System.Globalization.CultureInfo resourceCulture;\n        \n        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(\"Microsoft.Performance\", \"CA1811:AvoidUncalledPrivateCode\")]\n        internal Resources() {\n        }\n        \n        /// <summary>\n        ///   Returns the cached ResourceManager instance used by this class.\n        /// </summary>\n        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]\n        internal static global::System.Resources.ResourceManager ResourceManager {\n            get {\n                if (object.ReferenceEquals(resourceMan, null)) {\n                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager(\"DnsServerSystemTrayApp.Properties.Resources\", typeof(Resources).Assembly);\n                    resourceMan = temp;\n                }\n                return resourceMan;\n            }\n        }\n        \n        /// <summary>\n        ///   Overrides the current thread's CurrentUICulture property for all\n        ///   resource lookups using this strongly typed resource class.\n        /// </summary>\n        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]\n        internal static global::System.Globalization.CultureInfo Culture {\n            get {\n                return resourceCulture;\n            }\n            set {\n                resourceCulture = value;\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to A&amp;bout.\n        /// </summary>\n        internal static string AboutMenuItem {\n            get {\n                return ResourceManager.GetString(\"AboutMenuItem\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to Are you sure to close the system tray icon?\n        ///\n        ///Closing the system tray icon will not stop Technitium DNS Server..\n        /// </summary>\n        internal static string AreYouSureYouWantToQuit {\n            get {\n                return ResourceManager.GetString(\"AreYouSureYouWantToQuit\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to &amp;Dashboard.\n        /// </summary>\n        internal static string DashboardMenuItem {\n            get {\n                return ResourceManager.GetString(\"DashboardMenuItem\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to E&amp;xit.\n        /// </summary>\n        internal static string ExitMenuItem {\n            get {\n                return ResourceManager.GetString(\"ExitMenuItem\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized resource of type System.Drawing.Bitmap.\n        /// </summary>\n        internal static System.Drawing.Bitmap logo {\n            get {\n                object obj = ResourceManager.GetObject(\"logo\", resourceCulture);\n                return ((System.Drawing.Bitmap)(obj));\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized resource of type System.Drawing.Icon similar to (Icon).\n        /// </summary>\n        internal static System.Drawing.Icon logo2 {\n            get {\n                object obj = ResourceManager.GetObject(\"logo2\", resourceCulture);\n                return ((System.Drawing.Icon)(obj));\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to Network DNS.\n        /// </summary>\n        internal static string NetworkDnsMenuItem {\n            get {\n                return ResourceManager.GetString(\"NetworkDnsMenuItem\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to Close System Tray Icon?.\n        /// </summary>\n        internal static string Quit {\n            get {\n                return ResourceManager.GetString(\"Quit\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to &amp;Service.\n        /// </summary>\n        internal static string ServiceMenuItem {\n            get {\n                return ResourceManager.GetString(\"ServiceMenuItem\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to Technitium DNS Server.\n        /// </summary>\n        internal static string ServiceName {\n            get {\n                return ResourceManager.GetString(\"ServiceName\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to R&amp;estart.\n        /// </summary>\n        internal static string ServiceRestartMenuItem {\n            get {\n                return ResourceManager.GetString(\"ServiceRestartMenuItem\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to &amp;Start.\n        /// </summary>\n        internal static string ServiceStartMenuItem {\n            get {\n                return ResourceManager.GetString(\"ServiceStartMenuItem\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to St&amp;op.\n        /// </summary>\n        internal static string ServiceStopMenuItem {\n            get {\n                return ResourceManager.GetString(\"ServiceStopMenuItem\", resourceCulture);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/Properties/Resources.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"AboutMenuItem\" xml:space=\"preserve\">\n    <value>A&amp;bout</value>\n  </data>\n  <data name=\"AreYouSureYouWantToQuit\" xml:space=\"preserve\">\n    <value>Are you sure to close the system tray icon?\n\nClosing the system tray icon will not stop Technitium DNS Server.</value>\n  </data>\n  <data name=\"DashboardMenuItem\" xml:space=\"preserve\">\n    <value>&amp;Dashboard</value>\n  </data>\n  <data name=\"ExitMenuItem\" xml:space=\"preserve\">\n    <value>E&amp;xit</value>\n  </data>\n  <assembly alias=\"System.Windows.Forms\" name=\"System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\" />\n  <data name=\"logo\" type=\"System.Resources.ResXFileRef, System.Windows.Forms\">\n    <value>..\\logo.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>\n  </data>\n  <data name=\"logo2\" type=\"System.Resources.ResXFileRef, System.Windows.Forms\">\n    <value>..\\logo2.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>\n  </data>\n  <data name=\"NetworkDnsMenuItem\" xml:space=\"preserve\">\n    <value>Network DNS</value>\n  </data>\n  <data name=\"Quit\" xml:space=\"preserve\">\n    <value>Close System Tray Icon?</value>\n  </data>\n  <data name=\"ServiceMenuItem\" xml:space=\"preserve\">\n    <value>&amp;Service</value>\n  </data>\n  <data name=\"ServiceName\" xml:space=\"preserve\">\n    <value>Technitium DNS Server</value>\n  </data>\n  <data name=\"ServiceRestartMenuItem\" xml:space=\"preserve\">\n    <value>R&amp;estart</value>\n  </data>\n  <data name=\"ServiceStartMenuItem\" xml:space=\"preserve\">\n    <value>&amp;Start</value>\n  </data>\n  <data name=\"ServiceStopMenuItem\" xml:space=\"preserve\">\n    <value>St&amp;op</value>\n  </data>\n</root>"
  },
  {
    "path": "DnsServerSystemTrayApp/frmAbout.Designer.cs",
    "content": "﻿namespace DnsServerSystemTrayApp\n{\n    partial class frmAbout\n    {\n        /// <summary>\n        /// Required designer variable.\n        /// </summary>\n        private System.ComponentModel.IContainer components = null;\n\n        /// <summary>\n        /// Clean up any resources being used.\n        /// </summary>\n        /// <param name=\"disposing\">true if managed resources should be disposed; otherwise, false.</param>\n        protected override void Dispose(bool disposing)\n        {\n            if (disposing && (components != null))\n            {\n                components.Dispose();\n            }\n            base.Dispose(disposing);\n        }\n\n        #region Windows Form Designer generated code\n\n        /// <summary>\n        /// Required method for Designer support - do not modify\n        /// the contents of this method with the code editor.\n        /// </summary>\n        private void InitializeComponent()\n        {\n            System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frmAbout));\n            panel1 = new System.Windows.Forms.Panel();\n            pictureBox1 = new System.Windows.Forms.PictureBox();\n            label2 = new System.Windows.Forms.Label();\n            label4 = new System.Windows.Forms.Label();\n            lnkTerms = new System.Windows.Forms.LinkLabel();\n            btnClose = new System.Windows.Forms.Button();\n            label3 = new System.Windows.Forms.Label();\n            lnkWebsite = new System.Windows.Forms.LinkLabel();\n            label1 = new System.Windows.Forms.Label();\n            lnkContactEmail = new System.Windows.Forms.LinkLabel();\n            labVersion = new System.Windows.Forms.Label();\n            label5 = new System.Windows.Forms.Label();\n            panel1.SuspendLayout();\n            ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();\n            SuspendLayout();\n            // \n            // panel1\n            // \n            panel1.BackColor = System.Drawing.Color.FromArgb(102, 153, 255);\n            panel1.Controls.Add(pictureBox1);\n            panel1.Dock = System.Windows.Forms.DockStyle.Left;\n            panel1.Location = new System.Drawing.Point(0, 0);\n            panel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);\n            panel1.Name = \"panel1\";\n            panel1.Size = new System.Drawing.Size(58, 301);\n            panel1.TabIndex = 21;\n            // \n            // pictureBox1\n            // \n            pictureBox1.BackColor = System.Drawing.Color.FromArgb(102, 153, 255);\n            pictureBox1.Dock = System.Windows.Forms.DockStyle.Bottom;\n            pictureBox1.Image = Properties.Resources.logo;\n            pictureBox1.Location = new System.Drawing.Point(0, 243);\n            pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);\n            pictureBox1.Name = \"pictureBox1\";\n            pictureBox1.Padding = new System.Windows.Forms.Padding(5);\n            pictureBox1.Size = new System.Drawing.Size(58, 58);\n            pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize;\n            pictureBox1.TabIndex = 12;\n            pictureBox1.TabStop = false;\n            // \n            // label2\n            // \n            label2.AutoSize = true;\n            label2.Font = new System.Drawing.Font(\"Arial\", 30F, System.Drawing.FontStyle.Bold);\n            label2.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69);\n            label2.Location = new System.Drawing.Point(88, 28);\n            label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            label2.Name = \"label2\";\n            label2.Size = new System.Drawing.Size(456, 46);\n            label2.TabIndex = 24;\n            label2.Text = \"Technitium DNS Server\";\n            // \n            // label4\n            // \n            label4.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;\n            label4.Font = new System.Drawing.Font(\"Arial\", 8F);\n            label4.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69);\n            label4.Location = new System.Drawing.Point(72, 223);\n            label4.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            label4.Name = \"label4\";\n            label4.Size = new System.Drawing.Size(509, 51);\n            label4.TabIndex = 33;\n            label4.Text = resources.GetString(\"label4.Text\");\n            // \n            // lnkTerms\n            // \n            lnkTerms.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;\n            lnkTerms.AutoSize = true;\n            lnkTerms.Font = new System.Drawing.Font(\"Arial\", 9F);\n            lnkTerms.LinkColor = System.Drawing.Color.FromArgb(102, 153, 255);\n            lnkTerms.Location = new System.Drawing.Point(72, 273);\n            lnkTerms.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            lnkTerms.Name = \"lnkTerms\";\n            lnkTerms.Size = new System.Drawing.Size(116, 15);\n            lnkTerms.TabIndex = 32;\n            lnkTerms.TabStop = true;\n            lnkTerms.Text = \"Terms && Conditions\";\n            lnkTerms.VisitedLinkColor = System.Drawing.Color.White;\n            lnkTerms.LinkClicked += lnkTerms_LinkClicked;\n            // \n            // btnClose\n            // \n            btnClose.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;\n            btnClose.DialogResult = System.Windows.Forms.DialogResult.Cancel;\n            btnClose.Location = new System.Drawing.Point(659, 264);\n            btnClose.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);\n            btnClose.Name = \"btnClose\";\n            btnClose.Size = new System.Drawing.Size(88, 27);\n            btnClose.TabIndex = 31;\n            btnClose.Text = \"&Close\";\n            btnClose.UseVisualStyleBackColor = true;\n            // \n            // label3\n            // \n            label3.AutoSize = true;\n            label3.Font = new System.Drawing.Font(\"Arial\", 10F);\n            label3.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69);\n            label3.Location = new System.Drawing.Point(555, 166);\n            label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            label3.Name = \"label3\";\n            label3.Size = new System.Drawing.Size(58, 16);\n            label3.TabIndex = 37;\n            label3.Text = \"Website\";\n            // \n            // lnkWebsite\n            // \n            lnkWebsite.AutoSize = true;\n            lnkWebsite.Font = new System.Drawing.Font(\"Arial\", 10F);\n            lnkWebsite.LinkColor = System.Drawing.Color.FromArgb(102, 153, 255);\n            lnkWebsite.Location = new System.Drawing.Point(555, 185);\n            lnkWebsite.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            lnkWebsite.Name = \"lnkWebsite\";\n            lnkWebsite.Size = new System.Drawing.Size(128, 16);\n            lnkWebsite.TabIndex = 36;\n            lnkWebsite.TabStop = true;\n            lnkWebsite.Text = \"technitium.com/dns\";\n            lnkWebsite.VisitedLinkColor = System.Drawing.Color.White;\n            lnkWebsite.LinkClicked += lnkWebsite_LinkClicked;\n            // \n            // label1\n            // \n            label1.AutoSize = true;\n            label1.Font = new System.Drawing.Font(\"Arial\", 10F);\n            label1.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69);\n            label1.Location = new System.Drawing.Point(555, 114);\n            label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            label1.Name = \"label1\";\n            label1.Size = new System.Drawing.Size(56, 16);\n            label1.TabIndex = 35;\n            label1.Text = \"Contact\";\n            // \n            // lnkContactEmail\n            // \n            lnkContactEmail.AutoSize = true;\n            lnkContactEmail.Font = new System.Drawing.Font(\"Arial\", 10F);\n            lnkContactEmail.LinkColor = System.Drawing.Color.FromArgb(102, 153, 255);\n            lnkContactEmail.Location = new System.Drawing.Point(555, 133);\n            lnkContactEmail.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            lnkContactEmail.Name = \"lnkContactEmail\";\n            lnkContactEmail.Size = new System.Drawing.Size(163, 16);\n            lnkContactEmail.TabIndex = 34;\n            lnkContactEmail.TabStop = true;\n            lnkContactEmail.Text = \"support@technitium.com\";\n            lnkContactEmail.VisitedLinkColor = System.Drawing.Color.White;\n            lnkContactEmail.LinkClicked += lnkContactEmail_LinkClicked;\n            // \n            // labVersion\n            // \n            labVersion.AutoSize = true;\n            labVersion.Font = new System.Drawing.Font(\"Arial\", 12F);\n            labVersion.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69);\n            labVersion.Location = new System.Drawing.Point(100, 152);\n            labVersion.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            labVersion.Name = \"labVersion\";\n            labVersion.Size = new System.Drawing.Size(102, 18);\n            labVersion.TabIndex = 38;\n            labVersion.Text = \"version x.x.x.x\";\n            // \n            // label5\n            // \n            label5.AutoSize = true;\n            label5.Font = new System.Drawing.Font(\"Arial\", 18F, System.Drawing.FontStyle.Bold);\n            label5.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69);\n            label5.Location = new System.Drawing.Point(98, 119);\n            label5.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);\n            label5.Name = \"label5\";\n            label5.Size = new System.Drawing.Size(206, 29);\n            label5.TabIndex = 39;\n            label5.Text = \"System Tray App\";\n            // \n            // frmAbout\n            // \n            AcceptButton = btnClose;\n            AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);\n            AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;\n            BackColor = System.Drawing.Color.FromArgb(250, 250, 250);\n            CancelButton = btnClose;\n            ClientSize = new System.Drawing.Size(761, 301);\n            Controls.Add(label5);\n            Controls.Add(labVersion);\n            Controls.Add(label3);\n            Controls.Add(lnkWebsite);\n            Controls.Add(label1);\n            Controls.Add(lnkContactEmail);\n            Controls.Add(label4);\n            Controls.Add(lnkTerms);\n            Controls.Add(btnClose);\n            Controls.Add(label2);\n            Controls.Add(panel1);\n            FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;\n            Icon = (System.Drawing.Icon)resources.GetObject(\"$this.Icon\");\n            Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);\n            MaximizeBox = false;\n            MinimizeBox = false;\n            Name = \"frmAbout\";\n            StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;\n            Text = \"About Technitium DNS Server\";\n            panel1.ResumeLayout(false);\n            panel1.PerformLayout();\n            ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit();\n            ResumeLayout(false);\n            PerformLayout();\n        }\n\n        #endregion\n        private System.Windows.Forms.Panel panel1;\n        private System.Windows.Forms.PictureBox pictureBox1;\n        private System.Windows.Forms.Label label2;\n        private System.Windows.Forms.Label label4;\n        private System.Windows.Forms.LinkLabel lnkTerms;\n        private System.Windows.Forms.Button btnClose;\n        private System.Windows.Forms.Label label3;\n        private System.Windows.Forms.LinkLabel lnkWebsite;\n        private System.Windows.Forms.Label label1;\n        private System.Windows.Forms.LinkLabel lnkContactEmail;\n        private System.Windows.Forms.Label labVersion;\n        private System.Windows.Forms.Label label5;\n    }\n}"
  },
  {
    "path": "DnsServerSystemTrayApp/frmAbout.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System.Diagnostics;\nusing System.Windows.Forms;\n\nnamespace DnsServerSystemTrayApp\n{\n    public partial class frmAbout : Form\n    {\n        public frmAbout()\n        {\n            InitializeComponent();\n\n            labVersion.Text = \"version \" + Application.ProductVersion;\n        }\n\n        private void lnkContactEmail_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)\n        {\n            ProcessStartInfo processInfo = new ProcessStartInfo(\"mailto:\" + lnkContactEmail.Text);\n\n            processInfo.UseShellExecute = true;\n            processInfo.Verb = \"open\";\n\n            Process.Start(processInfo);\n        }\n\n        private void lnkWebsite_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)\n        {\n            ProcessStartInfo processInfo = new ProcessStartInfo(@\"https://\" + lnkWebsite.Text);\n\n            processInfo.UseShellExecute = true;\n            processInfo.Verb = \"open\";\n\n            Process.Start(processInfo);\n        }\n\n        private void lnkTerms_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)\n        {\n            ProcessStartInfo processInfo = new ProcessStartInfo(@\"https://go.technitium.com/?id=24\");\n\n            processInfo.UseShellExecute = true;\n            processInfo.Verb = \"open\";\n\n            Process.Start(processInfo);\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/frmAbout.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--\n    Microsoft ResX Schema\n\n    Version 2.0\n\n    The primary goals of this format is to allow a simple XML format\n    that is mostly human readable. The generation and parsing of the\n    various data types are done through the TypeConverter classes\n    associated with the data types.\n\n    Example:\n\n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n\n    There are any number of \"resheader\" rows that contain simple\n    name/value pairs.\n\n    Each data row contains a name, and value. The row also contains a\n    type or mimetype. Type corresponds to a .NET class that support\n    text/value conversion through the TypeConverter architecture.\n    Classes that don't support this are serialized and stored with the\n    mimetype set.\n\n    The mimetype is used for serialized objects, and tells the\n    ResXResourceReader how to depersist the object. This is currently not\n    extensible. For a given mimetype the value must be set accordingly:\n\n    Note - application/x-microsoft.net.object.binary.base64 is the format\n    that the ResXResourceWriter will generate, however the reader can\n    read any of the formats listed below.\n\n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with\n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with\n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array\n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <metadata name=\"panel1.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"pictureBox1.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label2.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label4.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <data name=\"label4.Text\" xml:space=\"preserve\">\n    <value>Copyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\nThis 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:</value>\n  </data>\n  <metadata name=\"lnkTerms.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"btnClose.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label3.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"lnkWebsite.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label1.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"lnkContactEmail.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"labVersion.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label5.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"$this.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <assembly alias=\"System.Drawing\" name=\"System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a\" />\n  <data name=\"$this.Icon\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n    <value>\n        AAABAAUAICAQAAAAAADoAgAAVgAAACAgAAAAAAAAqAgAAD4DAAAwMAAAAAAAAKgOAADmCwAAEBAQAAAA\n        AAAoAQAAjhoAABAQAAAAAAAAaAUAALYbAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A\n        /wD//wAA////AMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzP//zP//////////////zMz/\n        /8z//////////////8zM///M///////////////MzP//zP//////////////zMz//8zMzMzMzMz//8zM\n        zMzM///MzMzMzMzM///MzMzMzP//////////zP//zP//zMz//////////8z//8z//8zM///////////M\n        ///M///MzP//////////zP//zP//zMz//8zMzMzMzMz//8z//8zM///MzMzMzMzM///M///MzP//zP//\n        zMzMzP//zP//zMz//8z//8zMzMz//8z//8zM///M///MzMzM///M///MzP//zP//zMzMzP//zP//zMz/\n        /8z//8zMzMzMzMz//8zM///M///MzMzMzMzM///MzP//zP//zP//////////zMz//8z//8z/////////\n        /8zM///M///M///////////MzP//zP//zP//////////zMzMzMz//8zMzMzMzMz//8zMzMzM///MzMzM\n        zMzM///MzP//////////////zP//zMz//////////////8z//8zM///////////////M///MzP//////\n        ////////zP//zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAEAA\n        AAABAAgAAAAAAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDA\n        wADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIiIgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8\n        /wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAAmQAAAMwAADMAAAAzMwAAM2YAADOZAAAz\n        zAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZMwAAmWYAAJmZAACZzAAAmf8AAMwAAADM\n        MwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMAMwAzAGYAMwCZADMAzAAzAP8AMzMAADMz\n        MwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNmzAAzZv8AM5kAADOZMwAzmWYAM5mZADOZ\n        zAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/ZgAz/5kAM//MADP//wBmAAAAZgAzAGYA\n        ZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz/wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZ\n        AABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbMzABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA\n        /wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkzzACZAP8AmWYAAJlmMwCZM2YAmWaZAJlm\n        zACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbMZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnM\n        ZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkzAADMMzMAzDNmAMwzmQDMM8wAzDP/AMxm\n        AADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZmQDMmcwAzJn/AMzMAADMzDMAzMxmAMzM\n        mQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwAMwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8z\n        mQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+ZAAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/M\n        AAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP//zABmZv8AZv9mAGb//wD/ZmYA/2b/AP//\n        ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSg\n        oACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1f/////V1f//////////////////\n        ///////////V1dXV/////9XV/////////////////////////////9XV1dX/////1dX/////////////\n        ////////////////1dXV1f/////V1f/////////////////////////////V1dXV/////9XV1dXV1dXV\n        1dXV1dXV/////9XV1dXV1dXV1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1dXV1dXV1f//////////\n        ///////////V1f/////V1f/////V1dXV/////////////////////9XV/////9XV/////9XV1dX/////\n        ////////////////1dX/////1dX/////1dXV1f/////////////////////V1f/////V1f/////V1dXV\n        /////9XV1dXV1dXV1dXV1dXV/////9XV/////9XV1dX/////1dXV1dXV1dXV1dXV1dX/////1dX/////\n        1dXV1f/////V1f/////V1dXV1dXV1f/////V1f/////V1dXV/////9XV/////9XV1dXV1dXV/////9XV\n        /////9XV1dX/////1dX/////1dXV1dXV1dX/////1dX/////1dXV1f/////V1f/////V1dXV1dXV1f//\n        ///V1f/////V1dXV/////9XV/////9XV1dXV1dXV1dXV1dXV/////9XV1dX/////1dX/////1dXV1dXV\n        1dXV1dXV1dX/////1dXV1f/////V1f/////V1f/////////////////////V1dXV/////9XV/////9XV\n        /////////////////////9XV1dX/////1dX/////1dX/////////////////////1dXV1f/////V1f//\n        ///V1f/////////////////////V1dXV1dXV1dXV/////9XV1dXV1dXV1dXV1dXV/////9XV1dXV1dXV\n        1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1f/////////////////////////////V1f/////V1dXV\n        /////////////////////////////9XV/////9XV1dX/////////////////////////////1dX/////\n        1dXV1f/////////////////////////////V1f/////V1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAwAAAAYAAAAAEA\n        CAAAAAAAgAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDc\n        wADwyqYABAQEAAgICAAMDAwAERERABYWFgAcHBwAIiIiACkpKQBVVVUATU1NAEJCQgA5OTkAgHz/AFBQ\n        /wCTANYA/+zMAMbW7wDW5+cAkKmtAAAAMwAAAGYAAACZAAAAzAAAMwAAADMzAAAzZgAAM5kAADPMAAAz\n        /wAAZgAAAGYzAABmZgAAZpkAAGbMAABm/wAAmQAAAJkzAACZZgAAmZkAAJnMAACZ/wAAzAAAAMwzAADM\n        ZgAAzJkAAMzMAADM/wAA/2YAAP+ZAAD/zAAzAAAAMwAzADMAZgAzAJkAMwDMADMA/wAzMwAAMzMzADMz\n        ZgAzM5kAMzPMADMz/wAzZgAAM2YzADNmZgAzZpkAM2bMADNm/wAzmQAAM5kzADOZZgAzmZkAM5nMADOZ\n        /wAzzAAAM8wzADPMZgAzzJkAM8zMADPM/wAz/zMAM/9mADP/mQAz/8wAM///AGYAAABmADMAZgBmAGYA\n        mQBmAMwAZgD/AGYzAABmMzMAZjNmAGYzmQBmM8wAZjP/AGZmAABmZjMAZmZmAGZmmQBmZswAZpkAAGaZ\n        MwBmmWYAZpmZAGaZzABmmf8AZswAAGbMMwBmzJkAZszMAGbM/wBm/wAAZv8zAGb/mQBm/8wAzAD/AP8A\n        zACZmQAAmTOZAJkAmQCZAMwAmQAAAJkzMwCZAGYAmTPMAJkA/wCZZgAAmWYzAJkzZgCZZpkAmWbMAJkz\n        /wCZmTMAmZlmAJmZmQCZmcwAmZn/AJnMAACZzDMAZsxmAJnMmQCZzMwAmcz/AJn/AACZ/zMAmcxmAJn/\n        mQCZ/8wAmf//AMwAAACZADMAzABmAMwAmQDMAMwAmTMAAMwzMwDMM2YAzDOZAMwzzADMM/8AzGYAAMxm\n        MwCZZmYAzGaZAMxmzACZZv8AzJkAAMyZMwDMmWYAzJmZAMyZzADMmf8AzMwAAMzMMwDMzGYAzMyZAMzM\n        zADMzP8AzP8AAMz/MwCZ/2YAzP+ZAMz/zADM//8AzAAzAP8AZgD/AJkAzDMAAP8zMwD/M2YA/zOZAP8z\n        zAD/M/8A/2YAAP9mMwDMZmYA/2aZAP9mzADMZv8A/5kAAP+ZMwD/mWYA/5mZAP+ZzAD/mf8A/8wAAP/M\n        MwD/zGYA/8yZAP/MzAD/zP8A//8zAMz/ZgD//5kA///MAGZm/wBm/2YAZv//AP9mZgD/Zv8A//9mACEA\n        pQBfX18Ad3d3AIaGhgCWlpYAy8vLALKysgDX19cA3d3dAOPj4wDq6uoA8fHxAPj4+ADw+/8ApKCgAICA\n        gAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8A1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////\n        ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV////////\n        ////////////////////////////////////1dXV1dXV////////1dXV////////////////////////\n        ////////////////////1dXV1dXV////////1dXV////////////////////////////////////////\n        ////1dXV1dXV////////1dXV////////////////////////////////////////////1dXV1dXV////\n        ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        ////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV\n        1dXV1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV////\n        ////////////////////////////1dXV////////1dXV////////1dXV1dXV////////////////////\n        ////////////1dXV////////1dXV////////1dXV1dXV////////////////////////////////1dXV\n        ////////1dXV////////1dXV1dXV////////////////////////////////1dXV////////1dXV////\n        ////1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV////\n        ////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////\n        ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////\n        ////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV////////\n        1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV\n        ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////\n        ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////\n        ////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV////////\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV////////1dXV////////////\n        ////////////////////1dXV1dXV////////1dXV////////1dXV////////////////////////////\n        ////1dXV1dXV////////1dXV////////1dXV////////////////////////////////1dXV1dXV////\n        ////1dXV////////1dXV////////////////////////////////1dXV1dXV////////1dXV////////\n        1dXV////////////////////////////////1dXV1dXV////////1dXV////////1dXV////////////\n        ////////////////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////\n        ////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV\n        1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////////////////\n        ////////////////////////1dXV////////1dXV1dXV////////////////////////////////////\n        ////////1dXV////////1dXV1dXV////////////////////////////////////////////1dXV////\n        ////1dXV1dXV////////////////////////////////////////////1dXV////////1dXV1dXV////\n        ////////////////////////////////////////1dXV////////1dXV1dXV////////////////////\n        ////////////////////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXVAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAKAAAABAAAAAgAAAAAQAEAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAIAAAIAAAACAgACAAAAAgACAAICAAADAwMAAgICAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP//\n        /wDMzMzMzMzMzM/8///////8z/z///////zP/MzMzP/MzM/////8/8/8z/////z/z/zP/MzMzP/P/M/8\n        /8zM/8/8z/z/zMz/z/zP/P/MzMzP/M/8/8/////8z/z/z/////zMzP/MzMzP/M///////8/8z///////\n        z/zMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        gAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIi\n        IgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8/wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAA\n        mQAAAMwAADMAAAAzMwAAM2YAADOZAAAzzAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZ\n        MwAAmWYAAJmZAACZzAAAmf8AAMwAAADMMwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMA\n        MwAzAGYAMwCZADMAzAAzAP8AMzMAADMzMwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNm\n        zAAzZv8AM5kAADOZMwAzmWYAM5mZADOZzAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/\n        ZgAz/5kAM//MADP//wBmAAAAZgAzAGYAZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz\n        /wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZAABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbM\n        zABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA/wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkz\n        zACZAP8AmWYAAJlmMwCZM2YAmWaZAJlmzACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbM\n        ZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnMZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkz\n        AADMMzMAzDNmAMwzmQDMM8wAzDP/AMxmAADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZ\n        mQDMmcwAzJn/AMzMAADMzDMAzMxmAMzMmQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwA\n        MwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8zmQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+Z\n        AAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/MAAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP//\n        zABmZv8AZv9mAGb//wD/ZmYA/2b/AP//ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d\n        3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSgoACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV\n        1dXV1dXV1dXV1dXV1dXV///V///////////////V1f//1f//////////////1dX//9XV1dXV1dX//9XV\n        1dXV///////////V///V///V1f//////////1f//1f//1dX//9XV1dXV1dX//9X//9XV///V///V1dXV\n        ///V///V1f//1f//1dXV1f//1f//1dX//9X//9XV1dXV1dX//9XV///V///V///////////V1f//1f//\n        1f//////////1dXV1dX//9XV1dXV1dX//9XV///////////////V///V1f//////////////1f//1dXV\n        1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAA\n</value>\n  </data>\n</root>"
  },
  {
    "path": "DnsServerSystemTrayApp/frmManageDnsProviders.Designer.cs",
    "content": "﻿namespace DnsServerSystemTrayApp\n{\n    partial class frmManageDnsProviders\n    {\n        /// <summary>\n        /// Required designer variable.\n        /// </summary>\n        private System.ComponentModel.IContainer components = null;\n\n        /// <summary>\n        /// Clean up any resources being used.\n        /// </summary>\n        /// <param name=\"disposing\">true if managed resources should be disposed; otherwise, false.</param>\n        protected override void Dispose(bool disposing)\n        {\n            if (disposing && (components != null))\n            {\n                components.Dispose();\n            }\n            base.Dispose(disposing);\n        }\n\n        #region Windows Form Designer generated code\n\n        /// <summary>\n        /// Required method for Designer support - do not modify\n        /// the contents of this method with the code editor.\n        /// </summary>\n        private void InitializeComponent()\n        {\n            System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frmManageDnsProviders));\n            this.listView1 = new System.Windows.Forms.ListView();\n            this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));\n            this.columnHeader2 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));\n            this.columnHeader3 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));\n            this.groupBox1 = new System.Windows.Forms.GroupBox();\n            this.btnDelete = new System.Windows.Forms.Button();\n            this.btnClear = new System.Windows.Forms.Button();\n            this.btnAddUpdate = new System.Windows.Forms.Button();\n            this.txtIpv6Addresses = new System.Windows.Forms.TextBox();\n            this.label3 = new System.Windows.Forms.Label();\n            this.txtIpv4Addresses = new System.Windows.Forms.TextBox();\n            this.label2 = new System.Windows.Forms.Label();\n            this.txtDnsProviderName = new System.Windows.Forms.TextBox();\n            this.label1 = new System.Windows.Forms.Label();\n            this.btnOK = new System.Windows.Forms.Button();\n            this.btnCancel = new System.Windows.Forms.Button();\n            this.btnRestoreDefaults = new System.Windows.Forms.Button();\n            this.groupBox1.SuspendLayout();\n            this.SuspendLayout();\n            // \n            // listView1\n            // \n            this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) \n            | System.Windows.Forms.AnchorStyles.Right)));\n            this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {\n            this.columnHeader1,\n            this.columnHeader2,\n            this.columnHeader3});\n            this.listView1.FullRowSelect = true;\n            this.listView1.HideSelection = false;\n            this.listView1.Location = new System.Drawing.Point(12, 12);\n            this.listView1.MultiSelect = false;\n            this.listView1.Name = \"listView1\";\n            this.listView1.Size = new System.Drawing.Size(660, 200);\n            this.listView1.Sorting = System.Windows.Forms.SortOrder.Ascending;\n            this.listView1.TabIndex = 0;\n            this.listView1.UseCompatibleStateImageBehavior = false;\n            this.listView1.View = System.Windows.Forms.View.Details;\n            this.listView1.SelectedIndexChanged += new System.EventHandler(this.listView1_SelectedIndexChanged);\n            // \n            // columnHeader1\n            // \n            this.columnHeader1.Text = \"DNS Provider\";\n            this.columnHeader1.Width = 150;\n            // \n            // columnHeader2\n            // \n            this.columnHeader2.Text = \"IPv4 Addresses\";\n            this.columnHeader2.Width = 240;\n            // \n            // columnHeader3\n            // \n            this.columnHeader3.Text = \"IPv6 Addresses\";\n            this.columnHeader3.Width = 240;\n            // \n            // groupBox1\n            // \n            this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) \n            | System.Windows.Forms.AnchorStyles.Right)));\n            this.groupBox1.Controls.Add(this.btnDelete);\n            this.groupBox1.Controls.Add(this.btnClear);\n            this.groupBox1.Controls.Add(this.btnAddUpdate);\n            this.groupBox1.Controls.Add(this.txtIpv6Addresses);\n            this.groupBox1.Controls.Add(this.label3);\n            this.groupBox1.Controls.Add(this.txtIpv4Addresses);\n            this.groupBox1.Controls.Add(this.label2);\n            this.groupBox1.Controls.Add(this.txtDnsProviderName);\n            this.groupBox1.Controls.Add(this.label1);\n            this.groupBox1.Location = new System.Drawing.Point(12, 216);\n            this.groupBox1.Name = \"groupBox1\";\n            this.groupBox1.Size = new System.Drawing.Size(661, 130);\n            this.groupBox1.TabIndex = 9;\n            this.groupBox1.TabStop = false;\n            // \n            // btnDelete\n            // \n            this.btnDelete.Enabled = false;\n            this.btnDelete.Location = new System.Drawing.Point(179, 97);\n            this.btnDelete.Name = \"btnDelete\";\n            this.btnDelete.Size = new System.Drawing.Size(75, 23);\n            this.btnDelete.TabIndex = 16;\n            this.btnDelete.Text = \"&Delete\";\n            this.btnDelete.UseVisualStyleBackColor = true;\n            this.btnDelete.Click += new System.EventHandler(this.btnDelete_Click);\n            // \n            // btnClear\n            // \n            this.btnClear.Location = new System.Drawing.Point(260, 97);\n            this.btnClear.Name = \"btnClear\";\n            this.btnClear.Size = new System.Drawing.Size(75, 23);\n            this.btnClear.TabIndex = 17;\n            this.btnClear.Text = \"&Clear\";\n            this.btnClear.UseVisualStyleBackColor = true;\n            this.btnClear.Click += new System.EventHandler(this.btnClear_Click);\n            // \n            // btnAddUpdate\n            // \n            this.btnAddUpdate.Location = new System.Drawing.Point(98, 97);\n            this.btnAddUpdate.Name = \"btnAddUpdate\";\n            this.btnAddUpdate.Size = new System.Drawing.Size(75, 23);\n            this.btnAddUpdate.TabIndex = 15;\n            this.btnAddUpdate.Text = \"&Add\";\n            this.btnAddUpdate.UseVisualStyleBackColor = true;\n            this.btnAddUpdate.Click += new System.EventHandler(this.btnAddUpdate_Click);\n            // \n            // txtIpv6Addresses\n            // \n            this.txtIpv6Addresses.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) \n            | System.Windows.Forms.AnchorStyles.Right)));\n            this.txtIpv6Addresses.Location = new System.Drawing.Point(98, 71);\n            this.txtIpv6Addresses.MaxLength = 255;\n            this.txtIpv6Addresses.Name = \"txtIpv6Addresses\";\n            this.txtIpv6Addresses.Size = new System.Drawing.Size(552, 20);\n            this.txtIpv6Addresses.TabIndex = 14;\n            // \n            // label3\n            // \n            this.label3.AutoSize = true;\n            this.label3.Location = new System.Drawing.Point(11, 74);\n            this.label3.Name = \"label3\";\n            this.label3.Size = new System.Drawing.Size(81, 13);\n            this.label3.TabIndex = 13;\n            this.label3.Text = \"IPv6 Addresses\";\n            // \n            // txtIpv4Addresses\n            // \n            this.txtIpv4Addresses.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) \n            | System.Windows.Forms.AnchorStyles.Right)));\n            this.txtIpv4Addresses.Location = new System.Drawing.Point(98, 45);\n            this.txtIpv4Addresses.MaxLength = 255;\n            this.txtIpv4Addresses.Name = \"txtIpv4Addresses\";\n            this.txtIpv4Addresses.Size = new System.Drawing.Size(552, 20);\n            this.txtIpv4Addresses.TabIndex = 12;\n            // \n            // label2\n            // \n            this.label2.AutoSize = true;\n            this.label2.Location = new System.Drawing.Point(11, 48);\n            this.label2.Name = \"label2\";\n            this.label2.Size = new System.Drawing.Size(81, 13);\n            this.label2.TabIndex = 11;\n            this.label2.Text = \"IPv4 Addresses\";\n            // \n            // txtDnsProviderName\n            // \n            this.txtDnsProviderName.Location = new System.Drawing.Point(98, 19);\n            this.txtDnsProviderName.MaxLength = 255;\n            this.txtDnsProviderName.Name = \"txtDnsProviderName\";\n            this.txtDnsProviderName.Size = new System.Drawing.Size(200, 20);\n            this.txtDnsProviderName.TabIndex = 10;\n            // \n            // label1\n            // \n            this.label1.AutoSize = true;\n            this.label1.Location = new System.Drawing.Point(20, 22);\n            this.label1.Name = \"label1\";\n            this.label1.Size = new System.Drawing.Size(72, 13);\n            this.label1.TabIndex = 9;\n            this.label1.Text = \"DNS Provider\";\n            // \n            // btnOK\n            // \n            this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));\n            this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK;\n            this.btnOK.Location = new System.Drawing.Point(517, 356);\n            this.btnOK.Name = \"btnOK\";\n            this.btnOK.Size = new System.Drawing.Size(75, 23);\n            this.btnOK.TabIndex = 10;\n            this.btnOK.Text = \"OK\";\n            this.btnOK.UseVisualStyleBackColor = true;\n            // \n            // btnCancel\n            // \n            this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));\n            this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;\n            this.btnCancel.Location = new System.Drawing.Point(598, 356);\n            this.btnCancel.Name = \"btnCancel\";\n            this.btnCancel.Size = new System.Drawing.Size(75, 23);\n            this.btnCancel.TabIndex = 11;\n            this.btnCancel.Text = \"Cancel\";\n            this.btnCancel.UseVisualStyleBackColor = true;\n            // \n            // btnRestoreDefaults\n            // \n            this.btnRestoreDefaults.Location = new System.Drawing.Point(12, 356);\n            this.btnRestoreDefaults.Name = \"btnRestoreDefaults\";\n            this.btnRestoreDefaults.Size = new System.Drawing.Size(100, 23);\n            this.btnRestoreDefaults.TabIndex = 12;\n            this.btnRestoreDefaults.Text = \"Restore &Defaults\";\n            this.btnRestoreDefaults.UseVisualStyleBackColor = true;\n            this.btnRestoreDefaults.Click += new System.EventHandler(this.btnRestoreDefaults_Click);\n            // \n            // frmManageDnsProviders\n            // \n            this.AcceptButton = this.btnOK;\n            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);\n            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;\n            this.CancelButton = this.btnCancel;\n            this.ClientSize = new System.Drawing.Size(684, 386);\n            this.Controls.Add(this.btnRestoreDefaults);\n            this.Controls.Add(this.btnCancel);\n            this.Controls.Add(this.btnOK);\n            this.Controls.Add(this.groupBox1);\n            this.Controls.Add(this.listView1);\n            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;\n            this.Icon = ((System.Drawing.Icon)(resources.GetObject(\"$this.Icon\")));\n            this.MaximizeBox = false;\n            this.MinimizeBox = false;\n            this.Name = \"frmManageDnsProviders\";\n            this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;\n            this.Text = \"Manage Network DNS Providers - Technitium DNS Server\";\n            this.Load += new System.EventHandler(this.frmManageDnsProviders_Load);\n            this.groupBox1.ResumeLayout(false);\n            this.groupBox1.PerformLayout();\n            this.ResumeLayout(false);\n\n        }\n\n        #endregion\n\n        private System.Windows.Forms.ListView listView1;\n        private System.Windows.Forms.ColumnHeader columnHeader1;\n        private System.Windows.Forms.ColumnHeader columnHeader2;\n        private System.Windows.Forms.ColumnHeader columnHeader3;\n        private System.Windows.Forms.GroupBox groupBox1;\n        private System.Windows.Forms.Button btnClear;\n        private System.Windows.Forms.Button btnAddUpdate;\n        private System.Windows.Forms.TextBox txtIpv6Addresses;\n        private System.Windows.Forms.Label label3;\n        private System.Windows.Forms.TextBox txtIpv4Addresses;\n        private System.Windows.Forms.Label label2;\n        private System.Windows.Forms.TextBox txtDnsProviderName;\n        private System.Windows.Forms.Label label1;\n        private System.Windows.Forms.Button btnOK;\n        private System.Windows.Forms.Button btnCancel;\n        private System.Windows.Forms.Button btnRestoreDefaults;\n        private System.Windows.Forms.Button btnDelete;\n    }\n}"
  },
  {
    "path": "DnsServerSystemTrayApp/frmManageDnsProviders.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2024  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Windows.Forms;\n\nnamespace DnsServerSystemTrayApp\n{\n    public partial class frmManageDnsProviders : Form\n    {\n        #region variables\n\n        static readonly char[] commaSeparator = new char[] { ',' };\n\n        readonly List<DnsProvider> _dnsProviders = new List<DnsProvider>();\n\n        #endregion\n\n        #region constructor\n\n        public frmManageDnsProviders(ICollection<DnsProvider> dnsProviders)\n        {\n            InitializeComponent();\n\n            _dnsProviders.AddRange(dnsProviders);\n        }\n\n        #endregion\n\n        #region private\n\n        private void RefreshDnsProvidersList()\n        {\n            listView1.SuspendLayout();\n\n            listView1.Items.Clear();\n\n            foreach (DnsProvider dnsProvider in _dnsProviders)\n            {\n                ListViewItem item = listView1.Items.Add(dnsProvider.Name);\n                item.SubItems.Add(dnsProvider.GetIpv4Addresses());\n                item.SubItems.Add(dnsProvider.GetIpv6Addresses());\n\n                item.Tag = dnsProvider;\n            }\n\n            listView1.ResumeLayout();\n        }\n\n        private void ClearForm()\n        {\n            txtDnsProviderName.Text = \"\";\n            txtIpv4Addresses.Text = \"\";\n            txtIpv6Addresses.Text = \"\";\n            btnAddUpdate.Text = \"Add\";\n            btnDelete.Enabled = false;\n        }\n\n        private void frmManageDnsProviders_Load(object sender, EventArgs e)\n        {\n            RefreshDnsProvidersList();\n        }\n\n        private void listView1_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (listView1.SelectedItems.Count > 0)\n            {\n                ListViewItem selectedItem = listView1.SelectedItems[0];\n\n                txtDnsProviderName.Text = selectedItem.Text;\n                txtIpv4Addresses.Text = selectedItem.SubItems[1].Text;\n                txtIpv6Addresses.Text = selectedItem.SubItems[2].Text;\n                btnAddUpdate.Text = \"&Update\";\n                btnDelete.Enabled = true;\n            }\n            else\n            {\n                ClearForm();\n            }\n        }\n\n        private void btnAddUpdate_Click(object sender, EventArgs e)\n        {\n            if (string.IsNullOrWhiteSpace(txtDnsProviderName.Text))\n            {\n                MessageBox.Show(\"Please enter a valid DNS Provider name.\", \"Missing DNS Provider!\", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\n                return;\n            }\n\n            List<IPAddress> addresses = new List<IPAddress>();\n\n            foreach (string item in txtIpv4Addresses.Text.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries))\n            {\n                if (IPAddress.TryParse(item.Trim(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetwork))\n                {\n                    addresses.Add(address);\n                }\n                else\n                {\n                    MessageBox.Show(\"Please enter a valid IPv4 address.\", \"Invalid IPv4 Address!\", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\n                    return;\n                }\n            }\n\n            foreach (string item in txtIpv6Addresses.Text.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries))\n            {\n                if (IPAddress.TryParse(item.Trim(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetworkV6))\n                {\n                    addresses.Add(address);\n                }\n                else\n                {\n                    MessageBox.Show(\"Please enter a valid IPv6 address.\", \"Invalid IPv6 Address!\", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\n                    return;\n                }\n            }\n\n            if (addresses.Count == 0)\n            {\n                MessageBox.Show(\"Please enter at least one valid DNS provider IP address.\", \"Missing DNS Provider IP Address!\", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\n                return;\n            }\n\n            if ((btnAddUpdate.Text != \"Add\") && (listView1.SelectedItems.Count > 0))\n            {\n                ListViewItem selectedItem = listView1.SelectedItems[0];\n                DnsProvider dnsProvider = selectedItem.Tag as DnsProvider;\n\n                dnsProvider.Name = txtDnsProviderName.Text.Trim();\n                dnsProvider.Addresses = addresses;\n            }\n            else\n            {\n                _dnsProviders.Add(new DnsProvider(txtDnsProviderName.Text.Trim(), addresses));\n            }\n\n            RefreshDnsProvidersList();\n            ClearForm();\n        }\n\n        private void btnDelete_Click(object sender, EventArgs e)\n        {\n            if (listView1.SelectedItems.Count > 0)\n            {\n                ListViewItem selectedItem = listView1.SelectedItems[0];\n                DnsProvider dnsProvider = selectedItem.Tag as DnsProvider;\n\n                _dnsProviders.Remove(dnsProvider);\n                listView1.Items.Remove(selectedItem);\n            }\n\n            RefreshDnsProvidersList();\n            ClearForm();\n        }\n\n        private void btnClear_Click(object sender, EventArgs e)\n        {\n            ClearForm();\n        }\n\n        private void btnRestoreDefaults_Click(object sender, EventArgs e)\n        {\n            _dnsProviders.Clear();\n            _dnsProviders.AddRange(DnsProvider.GetDefaultProviders());\n\n            RefreshDnsProvidersList();\n            ClearForm();\n        }\n\n        #endregion\n\n        #region properties\n\n        public List<DnsProvider> DnsProviders\n        { get { return _dnsProviders; } }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "DnsServerSystemTrayApp/frmManageDnsProviders.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <metadata name=\"listView1.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"groupBox1.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"btnDelete.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"btnClear.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"btnAddUpdate.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"txtIpv6Addresses.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label3.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"txtIpv4Addresses.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label2.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"txtDnsProviderName.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"label1.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"btnOK.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"btnCancel.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"btnRestoreDefaults.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"$this.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <assembly alias=\"System.Drawing\" name=\"System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a\" />\n  <data name=\"$this.Icon\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n    <value>\n        AAABAAUAICAQAAAAAADoAgAAVgAAACAgAAAAAAAAqAgAAD4DAAAwMAAAAAAAAKgOAADmCwAAEBAQAAAA\n        AAAoAQAAjhoAABAQAAAAAAAAaAUAALYbAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A\n        /wD//wAA////AMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzP//zP//////////////zMz/\n        /8z//////////////8zM///M///////////////MzP//zP//////////////zMz//8zMzMzMzMz//8zM\n        zMzM///MzMzMzMzM///MzMzMzP//////////zP//zP//zMz//////////8z//8z//8zM///////////M\n        ///M///MzP//////////zP//zP//zMz//8zMzMzMzMz//8z//8zM///MzMzMzMzM///M///MzP//zP//\n        zMzMzP//zP//zMz//8z//8zMzMz//8z//8zM///M///MzMzM///M///MzP//zP//zMzMzP//zP//zMz/\n        /8z//8zMzMzMzMz//8zM///M///MzMzMzMzM///MzP//zP//zP//////////zMz//8z//8z/////////\n        /8zM///M///M///////////MzP//zP//zP//////////zMzMzMz//8zMzMzMzMz//8zMzMzM///MzMzM\n        zMzM///MzP//////////////zP//zMz//////////////8z//8zM///////////////M///MzP//////\n        ////////zP//zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAEAA\n        AAABAAgAAAAAAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDA\n        wADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIiIgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8\n        /wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAAmQAAAMwAADMAAAAzMwAAM2YAADOZAAAz\n        zAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZMwAAmWYAAJmZAACZzAAAmf8AAMwAAADM\n        MwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMAMwAzAGYAMwCZADMAzAAzAP8AMzMAADMz\n        MwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNmzAAzZv8AM5kAADOZMwAzmWYAM5mZADOZ\n        zAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/ZgAz/5kAM//MADP//wBmAAAAZgAzAGYA\n        ZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz/wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZ\n        AABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbMzABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA\n        /wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkzzACZAP8AmWYAAJlmMwCZM2YAmWaZAJlm\n        zACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbMZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnM\n        ZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkzAADMMzMAzDNmAMwzmQDMM8wAzDP/AMxm\n        AADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZmQDMmcwAzJn/AMzMAADMzDMAzMxmAMzM\n        mQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwAMwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8z\n        mQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+ZAAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/M\n        AAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP//zABmZv8AZv9mAGb//wD/ZmYA/2b/AP//\n        ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSg\n        oACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1f/////V1f//////////////////\n        ///////////V1dXV/////9XV/////////////////////////////9XV1dX/////1dX/////////////\n        ////////////////1dXV1f/////V1f/////////////////////////////V1dXV/////9XV1dXV1dXV\n        1dXV1dXV/////9XV1dXV1dXV1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1dXV1dXV1f//////////\n        ///////////V1f/////V1f/////V1dXV/////////////////////9XV/////9XV/////9XV1dX/////\n        ////////////////1dX/////1dX/////1dXV1f/////////////////////V1f/////V1f/////V1dXV\n        /////9XV1dXV1dXV1dXV1dXV/////9XV/////9XV1dX/////1dXV1dXV1dXV1dXV1dX/////1dX/////\n        1dXV1f/////V1f/////V1dXV1dXV1f/////V1f/////V1dXV/////9XV/////9XV1dXV1dXV/////9XV\n        /////9XV1dX/////1dX/////1dXV1dXV1dX/////1dX/////1dXV1f/////V1f/////V1dXV1dXV1f//\n        ///V1f/////V1dXV/////9XV/////9XV1dXV1dXV1dXV1dXV/////9XV1dX/////1dX/////1dXV1dXV\n        1dXV1dXV1dX/////1dXV1f/////V1f/////V1f/////////////////////V1dXV/////9XV/////9XV\n        /////////////////////9XV1dX/////1dX/////1dX/////////////////////1dXV1f/////V1f//\n        ///V1f/////////////////////V1dXV1dXV1dXV/////9XV1dXV1dXV1dXV1dXV/////9XV1dXV1dXV\n        1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1f/////////////////////////////V1f/////V1dXV\n        /////////////////////////////9XV/////9XV1dX/////////////////////////////1dX/////\n        1dXV1f/////////////////////////////V1f/////V1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAwAAAAYAAAAAEA\n        CAAAAAAAgAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDc\n        wADwyqYABAQEAAgICAAMDAwAERERABYWFgAcHBwAIiIiACkpKQBVVVUATU1NAEJCQgA5OTkAgHz/AFBQ\n        /wCTANYA/+zMAMbW7wDW5+cAkKmtAAAAMwAAAGYAAACZAAAAzAAAMwAAADMzAAAzZgAAM5kAADPMAAAz\n        /wAAZgAAAGYzAABmZgAAZpkAAGbMAABm/wAAmQAAAJkzAACZZgAAmZkAAJnMAACZ/wAAzAAAAMwzAADM\n        ZgAAzJkAAMzMAADM/wAA/2YAAP+ZAAD/zAAzAAAAMwAzADMAZgAzAJkAMwDMADMA/wAzMwAAMzMzADMz\n        ZgAzM5kAMzPMADMz/wAzZgAAM2YzADNmZgAzZpkAM2bMADNm/wAzmQAAM5kzADOZZgAzmZkAM5nMADOZ\n        /wAzzAAAM8wzADPMZgAzzJkAM8zMADPM/wAz/zMAM/9mADP/mQAz/8wAM///AGYAAABmADMAZgBmAGYA\n        mQBmAMwAZgD/AGYzAABmMzMAZjNmAGYzmQBmM8wAZjP/AGZmAABmZjMAZmZmAGZmmQBmZswAZpkAAGaZ\n        MwBmmWYAZpmZAGaZzABmmf8AZswAAGbMMwBmzJkAZszMAGbM/wBm/wAAZv8zAGb/mQBm/8wAzAD/AP8A\n        zACZmQAAmTOZAJkAmQCZAMwAmQAAAJkzMwCZAGYAmTPMAJkA/wCZZgAAmWYzAJkzZgCZZpkAmWbMAJkz\n        /wCZmTMAmZlmAJmZmQCZmcwAmZn/AJnMAACZzDMAZsxmAJnMmQCZzMwAmcz/AJn/AACZ/zMAmcxmAJn/\n        mQCZ/8wAmf//AMwAAACZADMAzABmAMwAmQDMAMwAmTMAAMwzMwDMM2YAzDOZAMwzzADMM/8AzGYAAMxm\n        MwCZZmYAzGaZAMxmzACZZv8AzJkAAMyZMwDMmWYAzJmZAMyZzADMmf8AzMwAAMzMMwDMzGYAzMyZAMzM\n        zADMzP8AzP8AAMz/MwCZ/2YAzP+ZAMz/zADM//8AzAAzAP8AZgD/AJkAzDMAAP8zMwD/M2YA/zOZAP8z\n        zAD/M/8A/2YAAP9mMwDMZmYA/2aZAP9mzADMZv8A/5kAAP+ZMwD/mWYA/5mZAP+ZzAD/mf8A/8wAAP/M\n        MwD/zGYA/8yZAP/MzAD/zP8A//8zAMz/ZgD//5kA///MAGZm/wBm/2YAZv//AP9mZgD/Zv8A//9mACEA\n        pQBfX18Ad3d3AIaGhgCWlpYAy8vLALKysgDX19cA3d3dAOPj4wDq6uoA8fHxAPj4+ADw+/8ApKCgAICA\n        gAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8A1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////\n        ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV////////\n        ////////////////////////////////////1dXV1dXV////////1dXV////////////////////////\n        ////////////////////1dXV1dXV////////1dXV////////////////////////////////////////\n        ////1dXV1dXV////////1dXV////////////////////////////////////////////1dXV1dXV////\n        ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        ////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV\n        1dXV1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV////\n        ////////////////////////////1dXV////////1dXV////////1dXV1dXV////////////////////\n        ////////////1dXV////////1dXV////////1dXV1dXV////////////////////////////////1dXV\n        ////////1dXV////////1dXV1dXV////////////////////////////////1dXV////////1dXV////\n        ////1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV////\n        ////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////\n        ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////\n        ////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV////////\n        1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV\n        ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////\n        ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////\n        ////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV////////\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV////////1dXV////////////\n        ////////////////////1dXV1dXV////////1dXV////////1dXV////////////////////////////\n        ////1dXV1dXV////////1dXV////////1dXV////////////////////////////////1dXV1dXV////\n        ////1dXV////////1dXV////////////////////////////////1dXV1dXV////////1dXV////////\n        1dXV////////////////////////////////1dXV1dXV////////1dXV////////1dXV////////////\n        ////////////////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////\n        ////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV\n        1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////////////////\n        ////////////////////////1dXV////////1dXV1dXV////////////////////////////////////\n        ////////1dXV////////1dXV1dXV////////////////////////////////////////////1dXV////\n        ////1dXV1dXV////////////////////////////////////////////1dXV////////1dXV1dXV////\n        ////////////////////////////////////////1dXV////////1dXV1dXV////////////////////\n        ////////////////////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV\n        1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXVAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAAKAAAABAAAAAgAAAAAQAEAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAIAAAIAAAACAgACAAAAAgACAAICAAADAwMAAgICAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP//\n        /wDMzMzMzMzMzM/8///////8z/z///////zP/MzMzP/MzM/////8/8/8z/////z/z/zP/MzMzP/P/M/8\n        /8zM/8/8z/z/zMz/z/zP/P/MzMzP/M/8/8/////8z/z/z/////zMzP/MzMzP/M///////8/8z///////\n        z/zMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        gAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIi\n        IgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8/wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAA\n        mQAAAMwAADMAAAAzMwAAM2YAADOZAAAzzAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZ\n        MwAAmWYAAJmZAACZzAAAmf8AAMwAAADMMwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMA\n        MwAzAGYAMwCZADMAzAAzAP8AMzMAADMzMwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNm\n        zAAzZv8AM5kAADOZMwAzmWYAM5mZADOZzAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/\n        ZgAz/5kAM//MADP//wBmAAAAZgAzAGYAZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz\n        /wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZAABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbM\n        zABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA/wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkz\n        zACZAP8AmWYAAJlmMwCZM2YAmWaZAJlmzACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbM\n        ZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnMZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkz\n        AADMMzMAzDNmAMwzmQDMM8wAzDP/AMxmAADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZ\n        mQDMmcwAzJn/AMzMAADMzDMAzMxmAMzMmQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwA\n        MwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8zmQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+Z\n        AAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/MAAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP//\n        zABmZv8AZv9mAGb//wD/ZmYA/2b/AP//ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d\n        3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSgoACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV\n        1dXV1dXV1dXV1dXV1dXV///V///////////////V1f//1f//////////////1dX//9XV1dXV1dX//9XV\n        1dXV///////////V///V///V1f//////////1f//1f//1dX//9XV1dXV1dX//9X//9XV///V///V1dXV\n        ///V///V1f//1f//1dXV1f//1f//1dX//9X//9XV1dXV1dX//9XV///V///V///////////V1f//1f//\n        1f//////////1dXV1dX//9XV1dXV1dX//9XV///////////////V///V1f//////////////1f//1dXV\n        1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n        AAAAAAAAAAAAAAAAAAAAAAAA\n</value>\n  </data>\n</root>"
  },
  {
    "path": "DnsServerWindowsService/DnsServerWindowsService.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Worker\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net9.0</TargetFramework>\n\t\t<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\t\t<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\t\t<GenerateAssemblyInfo>true</GenerateAssemblyInfo>\n\t\t<RootNamespace>DnsServerWindowsService</RootNamespace>\n\t\t<AssemblyName>DnsService</AssemblyName>\n\t\t<ApplicationIcon>logo2.ico</ApplicationIcon>\n\t\t<Version>14.3</Version>\n\t\t<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\t\t<Authors>Shreyas Zare</Authors>\n\t\t<Company>Technitium</Company>\n\t\t<Product>Technitium DNS Server</Product>\n\t\t<Description></Description>\n\t\t<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>\n\t\t<PackageId>DnsServerWindowsService</PackageId>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<Reference Include=\"TechnitiumLibrary.Net.Firewall\">\n\t\t\t<HintPath>..\\..\\TechnitiumLibrary\\bin\\TechnitiumLibrary.Net.Firewall.dll</HintPath>\n\t\t</Reference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Microsoft.Extensions.Hosting\" Version=\"9.0.10\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Hosting.WindowsServices\" Version=\"9.0.10\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\DnsServerCore\\DnsServerCore.csproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t  <Folder Include=\"Properties\\PublishProfiles\\\" />\n\t</ItemGroup>\n\n</Project>"
  },
  {
    "path": "DnsServerWindowsService/DnsServiceWorker.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2025  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing DnsServerCore;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Win32;\nusing System;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing TechnitiumLibrary.Net.Firewall;\n\nnamespace DnsServerWindowsService\n{\n    public sealed class DnsServiceWorker : BackgroundService\n    {\n        readonly DnsWebService _service;\n\n        public DnsServiceWorker()\n        {\n            string configFolder = null;\n\n            string[] args = Environment.GetCommandLineArgs();\n            if (args.Length == 2)\n                configFolder = args[1];\n\n            _service = new DnsWebService(configFolder, new Uri(\"https://go.technitium.com/?id=43\"));\n        }\n\n        public override async Task StartAsync(CancellationToken cancellationToken)\n        {\n            CheckFirewallEntries();\n\n            await _service.StartAsync();\n        }\n\n        public override async Task StopAsync(CancellationToken cancellationToken)\n        {\n            await _service.StopAsync();\n        }\n\n        public override void Dispose()\n        {\n            if (_service != null)\n                _service.Dispose();\n        }\n\n        protected override Task ExecuteAsync(CancellationToken stoppingToken)\n        {\n            return Task.CompletedTask;\n        }\n\n        private static void CheckFirewallEntries()\n        {\n            bool autoFirewallEntry = true;\n\n            try\n            {\n#pragma warning disable CA1416 // Validate platform compatibility\n\n                using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@\"SOFTWARE\\Technitium\\DNS Server\", false))\n                {\n                    if (key is not null)\n                        autoFirewallEntry = Convert.ToInt32(key.GetValue(\"AutoFirewallEntry\", 1)) == 1;\n                }\n\n#pragma warning restore CA1416 // Validate platform compatibility\n            }\n            catch\n            { }\n\n            if (autoFirewallEntry)\n            {\n                string appPath = Assembly.GetEntryAssembly().Location;\n\n                if (appPath.EndsWith(\".dll\", StringComparison.OrdinalIgnoreCase))\n                    appPath = appPath.Substring(0, appPath.Length - 4) + \".exe\";\n\n                if (!WindowsFirewallEntryExists(appPath))\n                    AddWindowsFirewallEntry(appPath);\n            }\n        }\n\n        private static bool WindowsFirewallEntryExists(string appPath)\n        {\n            try\n            {\n                return WindowsFirewall.RuleExistsVista(\"\", appPath) == RuleStatus.Allowed;\n            }\n            catch\n            {\n                return false;\n            }\n        }\n\n        private static bool AddWindowsFirewallEntry(string appPath)\n        {\n            try\n            {\n                RuleStatus status = WindowsFirewall.RuleExistsVista(\"\", appPath);\n\n                switch (status)\n                {\n                    case RuleStatus.Blocked:\n                    case RuleStatus.Disabled:\n                        WindowsFirewall.RemoveRuleVista(\"\", appPath);\n                        break;\n\n                    case RuleStatus.Allowed:\n                        return true;\n                }\n\n                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);\n\n                return true;\n            }\n            catch\n            {\n                return false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerWindowsService/Program.cs",
    "content": "﻿/*\nTechnitium DNS Server\nCopyright (C) 2021  Shreyas Zare (shreyas@technitium.com)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\n\nnamespace DnsServerWindowsService\n{\n    static class Program\n    {\n        public static void Main(string[] args)\n        {\n            CreateHostBuilder(args).Build().Run();\n        }\n\n        public static IHostBuilder CreateHostBuilder(string[] args)\n        {\n            return Host.CreateDefaultBuilder(args)\n                .ConfigureServices((hostContext, services) =>\n                {\n                    services.AddHostedService<DnsServiceWorker>();\n                })\n                .UseWindowsService();\n        }\n    }\n}\n"
  },
  {
    "path": "DnsServerWindowsService/Properties/PublishProfiles/FolderProfile.pubxml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->\n<Project>\n  <PropertyGroup>\n    <Configuration>Release</Configuration>\n    <Platform>Any CPU</Platform>\n    <PublishDir>..\\DnsServerWindowsSetup\\publish</PublishDir>\n    <PublishProtocol>FileSystem</PublishProtocol>\n    <_TargetId>Folder</_TargetId>\n    <TargetFramework>net9.0</TargetFramework>\n    <SelfContained>false</SelfContained>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "DnsServerWindowsSetup/DnsServerSetup.iss",
    "content": "; Script generated by the Inno Setup Script Wizard.\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!\n\n#define MyAppName \"Technitium DNS Server\"\n#define MyAppVersion \"14.3\"\n#define MyAppPublisher \"Technitium\"\n#define MyAppURL \"https://technitium.com/dns/\"\n#define MyAppExeName \"DnsServerSystemTrayApp.exe\"\n\n[Setup]\n; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.\n; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)\nAppId={{1052DB5E-35BD-4F67-89CD-1F45A1688E77}\nAppName={#MyAppName}\nAppVersion={#MyAppVersion}\n;AppVerName={#MyAppName} {#MyAppVersion}\nAppPublisher={#MyAppPublisher}\nAppPublisherURL={#MyAppURL}\nAppSupportURL={#MyAppURL}\nAppUpdatesURL={#MyAppURL}\nVersionInfoVersion=2.2.0.0\nVersionInfoCopyright=\"Copyright (C) 2025 Technitium\"\nDefaultDirName={commonpf32}\\Technitium\\DNS Server\nDefaultGroupName={#MyAppName}\nDisableProgramGroupPage=yes\nPrivilegesRequired=admin\nOutputDir=.\\Release\nOutputBaseFilename=DnsServerSetup\nSetupIconFile=.\\logo.ico\nWizardSmallImageFile=.\\logo.bmp\nCompression=lzma\nSolidCompression=yes\nWizardStyle=modern\nUninstallDisplayIcon={app}\\{#MyAppExeName}\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\";\n\n[Files]\nSource: \".\\publish\\{#MyAppExeName}\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \".\\publish\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\n\n[Icons]\nName: \"{group}\\DNS Server App\"; Filename: \"{app}\\{#MyAppExeName}\"\nName: \"{group}\\Dashboard\"; Filename: \"http://localhost:5380/\"\nName: \"{group}\\{cm:ProgramOnTheWeb,{#MyAppName}}\"; Filename: \"{#MyAppURL}\"\nName: \"{group}\\{cm:UninstallProgram,{#MyAppName}}\"; Filename: \"{uninstallexe}\"\nName: \"{autodesktop}\\DNS Server App\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: desktopicon\n\n[Run]\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"--first-run\"; Description: \"{cm:LaunchProgram,{#StringChange(\"DNS Server App\", '&', '&&')}}\"; Flags: nowait postinstall skipifsilent runascurrentuser\n\n#include \"helper.iss\"\n#include \"legacy.iss\"\n#include \"dotnet.iss\"\n#include \"appinstall.iss\"\n\n[Code]\n{\n  Skips the tasks page if it is an upgrade install\n}\nfunction ShouldSkipPage(PageID: Integer): Boolean;\nbegin\n  Result := ((PageID = wpSelectTasks) or (PageID = wpSelectDir)) and (IsLegacyInstallerInstalled or IsUpgrade);\nend;\n\nfunction InitializeSetup: Boolean;\nbegin\n  CheckDotnetDependency;\n  Result := true;\nend;\n\nprocedure CurStepChanged(CurStep: TSetupStep);\nbegin\n  if CurStep = ssInstall then begin //Step happens just before installing files\n    WizardForm.StatusLabel.Caption := 'Stopping Tray App...';\n    KillTrayApp(); //Stop the tray app if running\n\n    if IsLegacyInstallerInstalled then \n    begin\n      WizardForm.StatusLabel.Caption := 'Stopping Service...';\n      DoStopService(); //Stop the service if running  \n\n      WizardForm.StatusLabel.Caption := 'Removing Legacy Installer...';\n      UninstallLegacyInstaller(); //Uninstall Legacy Installer if Installed already\n    end else \n    begin\n      WizardForm.StatusLabel.Caption := 'Uninstalling Service...';\n      DoRemoveService(); //Stop and remove the service if installed\n    end;\n  end;\n\n  if CurStep = ssPostInstall then begin //Step happens just after installing files\n    WizardForm.StatusLabel.Caption := 'Installing Service...';\n    DoInstallService(); //Install service after all files installed, if not a portable install\n  end;\nend;\n\nprocedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);\nbegin\n  if CurUninstallStep = usUninstall then //Step happens before processing uninstall log\n  begin\n    UninstallProgressForm.StatusLabel.Caption := 'Resetting Network DNS...';\n    ResetNetworkDNS(); //Reset Network DNS to default\n\n    UninstallProgressForm.StatusLabel.Caption := 'Stopping Tray App...';\n    KillTrayApp(); //Stop the tray app if running\n\n    UninstallProgressForm.StatusLabel.Caption := 'Uninstalling Service...';\n    DoRemoveService(); //Stop and remove the service\n  end;\nend;"
  },
  {
    "path": "DnsServerWindowsSetup/appinstall.iss",
    "content": "\n#include \"service.iss\"\n\n#define SERVICE_NAME \"DnsService\"\n#define SERVICE_FILE \"DnsService.exe\"\n#define SERVICE_DISPLAY_NAME \"Technitium DNS Server\"\n#define SERVICE_DESCRIPTION \"Technitium DNS Server\"\n#define TRAYAPP_FILENAME \"DnsServerSystemTrayApp.exe\"\n\n[Code]\n{\n  Kills the tray app\n}\nprocedure KillTrayApp;\nbegin\n  TaskKill('{#TRAYAPP_FILENAME}');\nend;\n\n{\n    Resets Network DNS to default\n}\nprocedure ResetNetworkDNS;\nvar\n    ResultCode: Integer;\nbegin\n    Exec(ExpandConstant('{app}\\{#TRAYAPP_FILENAME}'), '--network-dns-default-exit', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\nend;\n\n{\n  Stops the service\n}\nprocedure DoStopService();\nvar\n  stopCounter: Integer;\nbegin\n  stopCounter := 0;\n  if IsServiceInstalled('{#SERVICE_NAME}') then begin\n    Log('Service: Already installed');\n    if IsServiceRunning('{#SERVICE_NAME}') then begin\n      Log('Service: Already running, stopping service...');\n      StopService('{#SERVICE_NAME}');\n\n      while IsServiceRunning('{#SERVICE_NAME}') do\n      begin\n       if stopCounter > 2 then begin\n         Log('Service: Waited too long to stop, killing task...');\n         TaskKill('{#SERVICE_FILE}');\n         Log('Service: Task killed');\n         break;\n       end else begin\n         Log('Service: Waiting for stop');\n         Sleep(2000);\n         stopCounter := stopCounter + 1\n       end;\n      end;\n      if stopCounter < 3 then Log('Service: Stopped');\n    end;\n  end;\nend;\n\n{\n  Removes the service from the computer\n}\nprocedure DoRemoveService();\nvar\n  stopCounter: Integer;\nbegin\n  stopCounter := 0;\n  if IsServiceInstalled('{#SERVICE_NAME}') then begin\n    Log('Service: Already installed, begin remove...');\n    if IsServiceRunning('{#SERVICE_NAME}') then begin\n      Log('Service: Already running, stopping...');\n      StopService('{#SERVICE_NAME}');\n      while IsServiceRunning('{#SERVICE_NAME}') do\n      begin\n        if stopCounter > 2 then begin\n          Log('Service: Waited too long to stop, killing task...');\n          TaskKill('{#SERVICE_FILE}');\n          Log('Service: Task killed');\n          break;\n        end else begin\n          Log('Service: Waiting for stop');\n          Sleep(5000);\n          stopCounter := stopCounter + 1\n        end;\n      end;\n    end;\n\n    stopCounter := 0;\n    Log('Service: Removing...');\n    RemoveService('{#SERVICE_NAME}');\n    while IsServiceInstalled('{#SERVICE_NAME}') do\n    begin\n      if stopCounter > 2 then begin\n        Log('Service: Waited too long to remove, continuing');\n        break;\n      end else begin\n        Log('Service: Waiting for removal');\n        Sleep(5000);\n        stopCounter := stopCounter + 1\n      end;\n    end;\n    if stopCounter < 3 then Log('Service: Removed');\n  end;\nend;\n\n{\n  Installs the service onto the computer\n}\nprocedure DoInstallService();\nvar\n  InstallSuccess: Boolean;\n  stopCounter: Integer;\nbegin\n  stopCounter := 0;\n  if IsServiceInstalled('{#SERVICE_NAME}') then begin\n    Log('Service: Already installed, skip install service');\n  end else begin \n    Log('Service: Begin Install');\n    InstallSuccess := InstallService(ExpandConstant('\"{app}\\DnsService.exe\"'), '{#SERVICE_NAME}', '{#SERVICE_DISPLAY_NAME}', '{#SERVICE_DESCRIPTION}', SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START);\n    if not InstallSuccess then\n    begin\n      Log('Service: Install Fail ' + ServiceErrorToMessage(GetLastError()));\n      SuppressibleMsgBox(ExpandConstant('{cm:ServiceInstallFailure,' + ServiceErrorToMessage(GetLastError()) + '}'), mbCriticalError, MB_OK, IDOK);\n    end else begin\n      Log('Service: Install Success, Starting...');\n      StartService('{#SERVICE_NAME}');\n\n      while IsServiceRunning('{#SERVICE_NAME}') <> true do\n      begin\n        if stopCounter > 3 then begin\n          Log('Service: Waited too long to start, continue');\n          break;\n        end else begin\n          Log('Service: still starting')\n          Sleep(5000);\n          stopCounter := stopCounter + 1\n        end;\n      end;\n      if stopCounter < 4 then Log('Service: Started');\n    end;\n  end;\nend;\n"
  },
  {
    "path": "DnsServerWindowsSetup/dotnet.iss",
    "content": "[Setup]\nMinVersion=6.1sp1\n\n// remove next line if you only deploy 32-bit binaries and dependencies\nArchitecturesInstallIn64BitMode=x64\n\n// dependency installation requires ready page and ready memo to be enabled (default behaviour)\nDisableReadyPage=no\nDisableReadyMemo=no\n\n// shared code for installing the dependencies\n[Code]\n// types and variables\ntype\n  TDependency = record\n    Filename: String;\n    Parameters: String;\n    Title: String;\n    URL: String;\n    Checksum: String;\n    ForceSuccess: Boolean;\n    InstallClean: Boolean;\n    RebootAfter: Boolean;\n  end;\n\n  InstallResult = (InstallSuccessful, InstallRebootRequired, InstallError);\n\nvar\n  MemoInstallInfo: String;\n  Dependencies: array of TDependency;\n  DelayedReboot, ForceX86: Boolean;\n  DownloadPage: TDownloadWizardPage;\n\nprocedure AddDependency(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, InstallClean, RebootAfter: Boolean);\nvar\n  Dependency: TDependency;\n  I: Integer;\nbegin\n  MemoInstallInfo := MemoInstallInfo + #13#10 + '%1' + Title;\n\n  Dependency.Filename := Filename;\n  Dependency.Parameters := Parameters;\n  Dependency.Title := Title;\n\n  if FileExists(ExpandConstant('{tmp}{\\}') + Filename) then begin\n    Dependency.URL := '';\n  end else begin\n    Dependency.URL := URL;\n  end;\n\n  Dependency.Checksum := Checksum;\n  Dependency.ForceSuccess := ForceSuccess;\n  Dependency.InstallClean := InstallClean;\n  Dependency.RebootAfter := RebootAfter;\n\n  I := GetArrayLength(Dependencies);\n  SetArrayLength(Dependencies, I + 1);\n  Dependencies[I] := Dependency;\nend;\n\nfunction IsPendingReboot: Boolean;\nvar\n  Value: String;\nbegin\n  Result := RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager', 'PendingFileRenameOperations', Value) or\n    (RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager', 'SetupExecute', Value) and (Value <> ''));\nend;\n\nfunction InstallProducts: InstallResult;\nvar\n  ResultCode, I, ProductCount: Integer;\nbegin\n  Result := InstallSuccessful;\n  ProductCount := GetArrayLength(Dependencies);\n  MemoInstallInfo := SetupMessage(msgReadyMemoTasks);\n\n  if ProductCount > 0 then begin\n    DownloadPage.Show;\n\n    for I := 0 to ProductCount - 1 do begin\n      if Dependencies[I].InstallClean and (DelayedReboot or IsPendingReboot) then begin\n        Result := InstallRebootRequired;\n        break;\n      end;\n\n      DownloadPage.SetText(Dependencies[I].Title, '');\n      DownloadPage.SetProgress(I + 1, ProductCount);\n\n      while True do begin\n        ResultCode := 0;\n        if ShellExec('', ExpandConstant('{tmp}{\\}') + Dependencies[I].Filename, Dependencies[I].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin\n          if Dependencies[I].RebootAfter then begin\n            // delay reboot after install if we installed the last dependency anyways\n            if I = ProductCount - 1 then begin\n              DelayedReboot := True;\n            end else begin\n              Result := InstallRebootRequired;\n              MemoInstallInfo := Dependencies[I].Title;\n            end;\n            break;\n          end else if (ResultCode = 0) or Dependencies[I].ForceSuccess then begin\n            break;\n          end else if ResultCode = 3010 then begin\n            // Windows Installer ResultCode 3010: ERROR_SUCCESS_REBOOT_REQUIRED\n            DelayedReboot := True;\n            break;\n          end;\n        end;\n\n        case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependencies[I].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of\n          IDABORT: begin\n            Result := InstallError;\n            MemoInstallInfo := MemoInstallInfo + #13#10 + '      ' + Dependencies[I].Title;\n            break;\n          end;\n          IDIGNORE: begin\n            MemoInstallInfo := MemoInstallInfo + #13#10 + '      ' + Dependencies[I].Title;\n            break;\n          end;\n        end;\n      end;\n\n      if Result <> InstallSuccessful then begin\n        break;\n      end;\n    end;\n\n    DownloadPage.Hide;\n  end;\nend;\n\n// Inno Setup event functions\nprocedure InitializeWizard;\nbegin\n  DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);\nend;\n\nfunction PrepareToInstall(var NeedsRestart: Boolean): String;\nbegin\n  DelayedReboot := False;\n\n  case InstallProducts of\n    InstallError: begin\n      Result := MemoInstallInfo;\n    end;\n    InstallRebootRequired: begin\n      Result := MemoInstallInfo;\n      NeedsRestart := True;\n\n      // write into the registry that the installer needs to be executed again after restart\n      RegWriteStringValue(HKEY_CURRENT_USER, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce', 'InstallBootstrap', ExpandConstant('{srcexe}'));\n    end;\n  end;\nend;\n\nfunction NeedRestart: Boolean;\nbegin\n  Result := DelayedReboot;\nend;\n\nfunction UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;\nbegin\n  Result := '';\n  if MemoUserInfoInfo <> '' then begin\n    Result := Result + MemoUserInfoInfo + Newline + NewLine;\n  end;\n  if MemoDirInfo <> '' then begin\n    Result := Result + MemoDirInfo + Newline + NewLine;\n  end;\n  if MemoTypeInfo <> '' then begin\n    Result := Result + MemoTypeInfo + Newline + NewLine;\n  end;\n  if MemoComponentsInfo <> '' then begin\n    Result := Result + MemoComponentsInfo + Newline + NewLine;\n  end;\n  if MemoGroupInfo <> '' then begin\n    Result := Result + MemoGroupInfo + Newline + NewLine;\n  end;\n  if MemoTasksInfo <> '' then begin\n    Result := Result + MemoTasksInfo;\n  end;\n\n  if MemoInstallInfo <> '' then begin\n    if MemoTasksInfo = '' then begin\n      Result := Result + SetupMessage(msgReadyMemoTasks);\n    end;\n    Result := Result + FmtMessage(MemoInstallInfo, [Space]);\n  end;\nend;\n\nfunction NextButtonClick(const CurPageID: Integer): Boolean;\nvar\n  I, ProductCount: Integer;\n  Retry: Boolean;\nbegin\n  Result := True;\n\n  if (CurPageID = wpReady) and (MemoInstallInfo <> '') then begin\n    DownloadPage.Show;\n\n    ProductCount := GetArrayLength(Dependencies);\n    for I := 0 to ProductCount - 1 do begin\n      if Dependencies[I].URL <> '' then begin\n        DownloadPage.Clear;\n        DownloadPage.Add(Dependencies[I].URL, Dependencies[I].Filename, Dependencies[I].Checksum);\n\n        Retry := True;\n        while Retry do begin\n          Retry := False;\n\n          try\n            DownloadPage.Download;\n          except\n            if GetExceptionMessage = SetupMessage(msgErrorDownloadAborted) then begin\n              Result := False;\n              I := ProductCount;\n            end else begin\n              case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of\n                IDABORT: begin\n                  Result := False;\n                  I := ProductCount;\n                end;\n                IDRETRY: begin\n                  Retry := True;\n                end;\n              end;\n            end;\n          end;\n        end;\n      end;\n    end;\n\n    DownloadPage.Hide;\n  end;\nend;\n\n// architecture helper functions\nfunction IsX64: Boolean;\nbegin\n  Result := not ForceX86 and Is64BitInstallMode;\nend;\n\nfunction GetString(const x86, x64: String): String;\nbegin\n  if IsX64 then begin\n    Result := x64;\n  end else begin\n    Result := x86;\n  end;\nend;\n\nfunction GetArchitectureSuffix: String;\nbegin\n  Result := GetString('', '_x64');\nend;\n\nfunction GetArchitectureTitle: String;\nbegin\n  Result := GetString(' (x86)', ' (x64)');\nend;\n\nfunction CompareVersion(const Version1, Version2: String): Integer;\nvar\n  Position, Number1, Number2: Integer;\nbegin\n  Result := 0;\n  while (Version1 <> '') or (Version2 <> '') do begin\n    Position := Pos('.', Version1);\n    if Position > 0 then begin\n      Number1 := StrToIntDef(Copy(Version1, 1, Position - 1), 0);\n      Delete(Version1, 1, Position);\n    end else if Version1 <> '' then begin\n      Number1 := StrToIntDef(Version1, 0);\n      Version1 := '';\n    end else begin\n      Number1 := 0;\n    end;\n\n    Position := Pos('.', Version2);\n    if Position > 0 then begin\n      Number2 := StrToIntDef(Copy(Version2, 1, Position - 1), 0);\n      Delete(Version2, 1, Position);\n    end else if Version2 <> '' then begin\n      Number2 := StrToIntDef(Version2, 0);\n      Version2 := '';\n    end else begin\n      Number2 := 0;\n    end;\n\n    if Number1 < Number2 then begin\n      Result := -1;\n      break;\n    end else if Number1 > Number2 then begin\n      Result := 1;\n      break;\n    end;\n  end;\nend;\n\n\n\n\n\n\n\n\n{ Check if dotnet is installed }\nfunction IsAspDotNetInstalled: Boolean;\nvar\n  ResultCode: Integer;\nbegin\n  Result := false;\n  Exec('cmd.exe', '/c dotnet --list-runtimes | find /n \"Microsoft.AspNetCore.App 9.0.11\"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n  if ResultCode = 0 then \n  begin \n    Result := true;\n  end;\nend;\n\nfunction IsDotNetDesktopInstalled: Boolean;\nvar\n  ResultCode: Integer;\nbegin\n  Result := false;\n  Exec('cmd.exe', '/c dotnet --list-runtimes | find /n \"Microsoft.WindowsDesktop.App 9.0.11\"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n  if ResultCode = 0 then \n  begin \n    Result := true;\n  end;\nend;\n\n{ if dotnet is not installed then add it for download }\nprocedure CheckDotnetDependency;\nbegin\n  if not IsAspDotNetInstalled then\n  begin\n    AddDependency('aspdotnet80' + GetArchitectureSuffix + '.exe',\n      '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',\n      'ASP.NET Core Runtime 9.0.11' + GetArchitectureTitle,\n      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'),\n      '', False, False, False);\n  end;\n\n  if not IsDotNetDesktopInstalled then\n  begin\n    AddDependency('dotnet80desktop' + GetArchitectureSuffix + '.exe',\n      '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',\n      '.NET Desktop Runtime 9.0.11' + GetArchitectureTitle,\n      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'),\n      '', False, False, False);\n  end;\nend;\n"
  },
  {
    "path": "DnsServerWindowsSetup/helper.iss",
    "content": "[Code]\n{\n    Helper functions\n}\n\n{\n    Checks to see if the installer is an 'upgrade'\n}\nfunction IsUpgrade: Boolean;\nvar\n    Value: string;\n    UninstallKey: string;\nbegin\n    UninstallKey := 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\' +\n        ExpandConstant('{#SetupSetting(\"AppId\")}') + '_is1';\n    Result := (RegQueryStringValue(HKLM, UninstallKey, 'UninstallString', Value) or\n        RegQueryStringValue(HKCU, UninstallKey, 'UninstallString', Value)) and (Value <> '');\nend;\n\n{\n    Kills a running program by its filename\n}\nprocedure TaskKill(fileName: String);\nvar\n    ResultCode: Integer;\nbegin\n    Exec(ExpandConstant('taskkill.exe'), '/f /im ' + '\"' + fileName + '\"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\nend;\n\n\n{\n    Executes the MSI Uninstall by GUID functionality\n}\nfunction MsiExecUnins(appId: String): Integer;\nvar \n  ResultCode: Integer;\nbegin\n  ShellExec('', 'msiexec.exe', '/x ' + appId + ' /norestart /qb', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n  Result := ResultCode;\nend;\n"
  },
  {
    "path": "DnsServerWindowsSetup/legacy.iss",
    "content": "#define LEGACY_INSTALLER_APPID \"{9B86AC7F-53B3-4E31-B245-D4602D16F5C8}\"\n\n[Code]\n\n{\n    Legacy Installer Functionality\n}\n\n{\n    Checks if the MSI Installer is installed\n}\nfunction IsLegacyInstallerInstalled: Boolean;\nvar\n  Value: string;\n  UninstallKey1, UninstallKey2: string;\nbegin\n  UninstallKey1 := 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{#LEGACY_INSTALLER_APPID}';\n  UninstallKey2 := 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{#LEGACY_INSTALLER_APPID}';\n  Result := (\n    RegQueryStringValue(HKLM, UninstallKey1, 'UninstallString', Value) or\n    RegQueryStringValue(HKCU, UninstallKey1, 'UninstallString', Value) or\n    RegQueryStringValue(HKLM, UninstallKey2, 'UninstallString', Value)\n    ) and (Value <> '');\nend;\n\n{\n    Uninstalls Legacy Installer\n}\nprocedure UninstallLegacyInstaller;\nvar\n  ResultCode: Integer;\nbegin\n    Log('Uninstall MSI installer item');\n    ResultCode := MsiExecUnins('{#LEGACY_INSTALLER_APPID}');\n    Log('Result code ' + IntToStr(ResultCode));\nend;\n"
  },
  {
    "path": "DnsServerWindowsSetup/service.iss",
    "content": "[Code]\n\ntype\n\tSERVICE_STATUS = record\n    \tdwServiceType\t\t\t\t: cardinal;\n    \tdwCurrentState\t\t\t\t: cardinal;\n    \tdwControlsAccepted\t\t\t: cardinal;\n    \tdwWin32ExitCode\t\t\t\t: cardinal;\n    \tdwServiceSpecificExitCode\t: cardinal;\n    \tdwCheckPoint\t\t\t\t: cardinal;\n    \tdwWaitHint\t\t\t\t\t: cardinal;\n\tend;\n\tHANDLE = cardinal;\n\nconst\n\tSERVICE_QUERY_CONFIG\t\t= $1;\n\tSERVICE_CHANGE_CONFIG\t\t= $2;\n\tSERVICE_QUERY_STATUS\t\t= $4;\n\tSERVICE_START\t\t\t\t= $10;\n\tSERVICE_STOP\t\t\t\t= $20;\n\tSERVICE_ALL_ACCESS\t\t\t= $f01ff;\n\tSC_MANAGER_ALL_ACCESS\t\t= $f003f;\n\tSERVICE_WIN32_OWN_PROCESS\t= $10;\n\tSERVICE_WIN32_SHARE_PROCESS\t= $20;\n\tSERVICE_WIN32\t\t\t\t= $30;\n\tSERVICE_INTERACTIVE_PROCESS = $100;\n\tSERVICE_BOOT_START          = $0;\n\tSERVICE_SYSTEM_START        = $1;\n\tSERVICE_AUTO_START          = $2;\n\tSERVICE_DEMAND_START        = $3;\n\tSERVICE_DISABLED            = $4;\n\tSERVICE_DELETE              = $10000;\n\tSERVICE_CONTROL_STOP\t\t= $1;\n\tSERVICE_CONTROL_PAUSE\t\t= $2;\n\tSERVICE_CONTROL_CONTINUE\t= $3;\n\tSERVICE_CONTROL_INTERROGATE = $4;\n\tSERVICE_STOPPED\t\t\t\t= $1;\n\tSERVICE_START_PENDING       = $2;\n\tSERVICE_STOP_PENDING        = $3;\n\tSERVICE_RUNNING             = $4;\n\tSERVICE_CONTINUE_PENDING    = $5;\n\tSERVICE_PAUSE_PENDING       = $6;\n\tSERVICE_PAUSED              = $7;\n\n\n\tERROR_ACCESS_DENIED               = 5;\n\tERROR_CIRCULAR_DEPENDENCY         = 1059;\n\tERROR_DUPLICATE_SERVICE_NAME      = 1078;\n\tERROR_INVALID_HANDLE              = 6;\n\tERROR_INVALID_NAME                = 123;\n\tERROR_INVALID_PARAMETER           = 87;\n\tERROR_INVALID_SERVICE_ACCOUNT     = 1057;\n\tERROR_SERVICE_EXISTS              = 1073;\n\tERROR_SERVICE_MARKED_FOR_DELETE   = 1072;\n\t\n// #######################################################################################\n// nt based service utilities\n// #######################################################################################\nfunction OpenSCManager(lpMachineName, lpDatabaseName: string; dwDesiredAccess :cardinal): HANDLE;\nexternal 'OpenSCManagerW@advapi32.dll stdcall';\n\nfunction OpenService(hSCManager :HANDLE;lpServiceName: string; dwDesiredAccess :cardinal): HANDLE;\nexternal 'OpenServiceW@advapi32.dll stdcall';\n\nfunction CloseServiceHandle(hSCObject :HANDLE): boolean;\nexternal 'CloseServiceHandle@advapi32.dll stdcall';\n\nfunction CreateService(hSCManager :HANDLE;lpServiceName, lpDisplayName: string;dwDesiredAccess,dwServiceType,dwStartType,dwErrorControl: cardinal;lpBinaryPathName,lpLoadOrderGroup: String; lpdwTagId : cardinal;lpDependencies,lpServiceStartName,lpPassword :string): cardinal;\nexternal 'CreateServiceW@advapi32.dll stdcall';\n\nfunction DeleteService(hService :HANDLE): boolean;\nexternal 'DeleteService@advapi32.dll stdcall';\n\nfunction StartNTService(hService :HANDLE;dwNumServiceArgs : cardinal;lpServiceArgVectors : cardinal) : boolean;\nexternal 'StartServiceW@advapi32.dll stdcall';\n\nfunction ControlService(hService :HANDLE; dwControl :cardinal;var ServiceStatus :SERVICE_STATUS) : boolean;\nexternal 'ControlService@advapi32.dll stdcall';\n\nfunction QueryServiceStatus(hService :HANDLE;var ServiceStatus :SERVICE_STATUS) : boolean;\nexternal 'QueryServiceStatus@advapi32.dll stdcall';\n\nfunction QueryServiceStatusEx(hService :HANDLE;ServiceStatus :SERVICE_STATUS) : boolean;\nexternal 'QueryServiceStatus@advapi32.dll stdcall';\n\nfunction GetLastError(): dword;\nexternal 'GetLastError@kernel32.dll stdcall';\n\nfunction OpenServiceManager(): HANDLE;\nbegin\n\tif UsingWinNT() = true then begin\n\t\tResult := OpenSCManager('', 'ServicesActive', SC_MANAGER_ALL_ACCESS);\n\t\tif Result = 0 then\n\t\t\tMsgBox(ExpandConstant('{cm:ServiceManagerUnavailable}'), mbError, MB_OK);\n\tend\n\telse begin\n        MsgBox('only nt based systems support services', mbError, MB_OK);\n        Result := 0;\n\tend\nend;\n\nfunction IsServiceInstalled(ServiceName: string): boolean;\nvar\n\thSCM\t: HANDLE;\n\thService: HANDLE;\nbegin\n\thSCM := OpenServiceManager();\n\tResult := false;\n\tif hSCM <> 0 then begin\n\t\thService := OpenService(hSCM, ServiceName, SERVICE_QUERY_CONFIG);\n        if hService <> 0 then begin\n            Result := true;\n            CloseServiceHandle(hService);\n\t\tend;\n        CloseServiceHandle(hSCM);\n\tend\nend;\n\nfunction InstallService(FileName, ServiceName, DisplayName, Description: string; ServiceType, StartType: cardinal): boolean;\nvar\n\thSCM\t: HANDLE;\n\thService: HANDLE;\nbegin\n\thSCM := OpenServiceManager();\n\tResult := false;\n\tif hSCM <> 0 then begin\n\t\thService := CreateService(hSCM, ServiceName, DisplayName, SERVICE_ALL_ACCESS, ServiceType, StartType, 0, FileName,'', 0, '', '', '');\n\t\tif hService <> 0 then begin\n\t\t\tResult := true;\n\t\t\t// Win2K & WinXP supports aditional description text for services\n\t\t\tif Description <> '' then\n\t\t\t\tRegWriteStringValue(HKLM,'System\\CurrentControlSet\\Services\\' + ServiceName, 'Description', Description);\n\t\t\tCloseServiceHandle(hService);\n\t\tend;\n        CloseServiceHandle(hSCM);\n\tend;\nend;\n\nfunction RemoveService(ServiceName: string): boolean;\nvar\n\thSCM\t: HANDLE;\n\thService: HANDLE;\nbegin\n\thSCM := OpenServiceManager();\n\tResult := false;\n\tif hSCM <> 0 then begin\n\t\thService := OpenService(hSCM, ServiceName, SERVICE_DELETE);\n        if hService <> 0 then begin\n            Result := DeleteService(hService);\n            CloseServiceHandle(hService);\n\t\tend;\n        CloseServiceHandle(hSCM);\n\tend;\nend;\n\nfunction StartService(ServiceName: string): boolean;\nvar\n\thSCM\t: HANDLE;\n\thService: HANDLE;\nbegin\n\thSCM := OpenServiceManager();\n\tResult := false;\n\tif hSCM <> 0 then begin\n\t\thService := OpenService(hSCM, ServiceName, SERVICE_START);\n        if hService <> 0 then begin\n        \tResult := StartNTService(hService, 0, 0);\n            CloseServiceHandle(hService);\n\t\tend;\n        CloseServiceHandle(hSCM);\n\tend;\nend;\n\nfunction StopService(ServiceName: string): boolean;\nvar\n\thSCM\t: HANDLE;\n\thService: HANDLE;\n\tStatus\t: SERVICE_STATUS;\nbegin\n\thSCM := OpenServiceManager();\n\tResult := false;\n\tif hSCM <> 0 then begin\n\t\thService := OpenService(hSCM, ServiceName, SERVICE_STOP);\n        if hService <> 0 then begin\n        \tResult := ControlService(hService, SERVICE_CONTROL_STOP, Status);\n            CloseServiceHandle(hService);\n\t\tend;\n        CloseServiceHandle(hSCM);\n\tend;\nend;\n\nfunction IsServiceRunning(ServiceName: string): boolean;\nvar\n\thSCM\t: HANDLE;\n\thService: HANDLE;\n\tStatus\t: SERVICE_STATUS;\nbegin\n\thSCM := OpenServiceManager();\n\tResult := false;\n\tif hSCM <> 0 then begin\n\t\thService := OpenService(hSCM, ServiceName, SERVICE_QUERY_STATUS);\n    \tif hService <> 0 then begin\n\t\t\tif QueryServiceStatus(hService, Status) then begin\n\t\t\t\tResult :=(Status.dwCurrentState = SERVICE_RUNNING);\n        \tend;\n            CloseServiceHandle(hService);\n\t\tend;\n        CloseServiceHandle(hSCM);\n\tend\nend;\n\nfunction ServiceErrorToMessage(Error: word): string;\nbegin\n\tcase Error of \n\t\tERROR_ACCESS_DENIED: Result := 'Access Denied';\n\t\tERROR_CIRCULAR_DEPENDENCY: Result := 'Circular Dependency';\n\t\tERROR_DUPLICATE_SERVICE_NAME: Result := 'Duplicate Service Name';\n\t\tERROR_INVALID_HANDLE: Result := 'Invalid Handle';\n\t\tERROR_INVALID_NAME: Result := 'Invalid Name';\n\t\tERROR_INVALID_PARAMETER: Result := 'Invalid Parameter';\n\t\tERROR_INVALID_SERVICE_ACCOUNT: Result := 'Invalid Service Account';\n\t\tERROR_SERVICE_EXISTS: Result := 'Service Exists';\n\t\tERROR_SERVICE_MARKED_FOR_DELETE: Result := 'Service Marked For Deletion';\n\telse\n\t\tResult := 'Unknown error: ' + IntToStr(Error);\n\tend;\nend;"
  },
  {
    "path": "DockerEnvironmentVariables.md",
    "content": "# Technitium DNS Server Docker Environment Variables\n\nTechnitium 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.\n\nNOTE! 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.\n\nThe environment variables are described below:\n\n| Environment Variable                           | Type    | Description                                                                                                                              |\n| ---------------------------------------------- | ------- | -----------------------------------------------------------------------------------------------------------------------------------------|\n| DNS_SERVER_DOMAIN                              | String  | The primary domain name used by this DNS Server to identify itself.                                                                      |\n| DNS_SERVER_ADMIN_PASSWORD                      | String  | The DNS web console admin user password.                                                                                                 |\n| DNS_SERVER_ADMIN_PASSWORD_FILE                 | String  | The path to a file that contains a plain text password for the DNS web console admin user.                                               |\n| DNS_SERVER_PREFER_IPV6                         | Boolean | DNS Server will use IPv6 for querying whenever possible with this option enabled.                                                        |\n| DNS_SERVER_WEB_SERVICE_LOCAL_ADDRESSES         | String  | A comma separated list of IP addresses for the DNS web console to listen on.                                                             |\n| DNS_SERVER_WEB_SERVICE_HTTP_PORT               | Integer | The TCP port number for the DNS web console over HTTP protocol.                                                                          |\n| DNS_SERVER_WEB_SERVICE_HTTPS_PORT              | Integer | The TCP port number for the DNS web console over HTTPS protocol.                                                                         |\n| DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS            | Boolean | Enables HTTPS for the DNS web console.                                                                                                   |\n| DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT    | Boolean | Enables self signed TLS certificate for the DNS web console.                                                                             |\n| DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PATH    | String  | The file path to the TLS certificate for the DNS web console.                                                                            |\n| DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PASSWORD| String  | The password for the TLS certificate for the DNS web console.                                                                            |\n| DNS_SERVER_WEB_SERVICE_HTTP_TO_TLS_REDIRECT    | Boolean | Enables HTTP to HTTPS redirection for the DNS web console.                                                                               |\n| 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.            |\n| DNS_SERVER_RECURSION                           | String  | Recursion options: `Allow`, `Deny`, `AllowOnlyForPrivateNetworks`, `UseSpecifiedNetworkACL`.                                             |\n| 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. |\n| 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.  |\n| 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. |\n| DNS_SERVER_ENABLE_BLOCKING                     | Boolean | Sets the DNS server to block domain names using Blocked Zone and Block List Zone.                                                        |\n| 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.                    |\n| DNS_SERVER_BLOCK_LIST_URLS                     | String  | A comma separated list of block list URLs.                                                                                               |\n| DNS_SERVER_FORWARDERS                          | String  | A comma separated list of forwarder addresses.                                                                                           |\n| DNS_SERVER_FORWARDER_PROTOCOL                  | String  | Forwarder protocol options: `Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`.                                                                   |\n| DNS_SERVER_LOG_USING_LOCAL_TIME                | Boolean | Enable this option to use local time instead of UTC for logging.                                                                         |\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker.io/docker/dockerfile:1\n\nFROM mcr.microsoft.com/dotnet/aspnet:9.0\n\n# Add the MS repo to install `libmsquic` to support DNS-over-QUIC:\nADD --link https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb /\nRUN <<HEREDOC\n  dpkg -i packages-microsoft-prod.deb && rm packages-microsoft-prod.deb\n  # `dnsutils` added to include the `dig` command for troubleshooting:\n  apt-get update && apt-get install -y libmsquic dnsutils\n  apt-get clean -y && rm -rf /var/lib/apt/lists/*\n\n  # `/etc/dns` is expected to exist the default directory for persisting state:\n  # (Users should volume mount to this location or modify the `CMD` of their container)\n  mkdir /etc/dns\nHEREDOC\n\n# Project is built outside of Docker, copy over the build directory:\nWORKDIR /opt/technitium/dns\nCOPY --link ./DnsServerApp/bin/Release/publish /opt/technitium/dns\n\nENTRYPOINT [\"/usr/bin/dotnet\", \"/opt/technitium/dns/DnsServerApp.dll\"]\nCMD [\"/etc/dns\"]\n\n\n## Only append image metadata below this line:\nEXPOSE \\\n  # Standard DNS service\n  53/udp 53/tcp      \\\n  # DNS-over-QUIC (UDP) + DNS-over-TLS (TCP)\n  853/udp 853/tcp    \\\n  # DNS-over-HTTPS (UDP => HTTP/3) (TCP => HTTP/1.1 + HTTP/2)\n  443/udp 443/tcp    \\\n  # DNS-over-HTTP (for when running behind a reverse-proxy that terminates TLS)\n  80/tcp 8053/tcp    \\\n  # Technitium web console + API (HTTP / HTTPS)\n  5380/tcp 53443/tcp \\\n  # DHCP\n  67/udp\n\n# https://specs.opencontainers.org/image-spec/annotations/\n# https://github.com/opencontainers/image-spec/blob/main/annotations.md\nLABEL org.opencontainers.image.title=\"Technitium DNS Server\"\nLABEL org.opencontainers.image.vendor=\"Technitium\"\nLABEL org.opencontainers.image.source=\"https://github.com/TechnitiumSoftware/DnsServer\"\nLABEL org.opencontainers.image.url=\"https://technitium.com/dns/\"\nLABEL org.opencontainers.image.authors=\"support@technitium.com\"\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    {one line to give the program's name and a brief idea of what it does.}\n    Copyright (C) {year}  {name of author}\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    {project}  Copyright (C) {year}  {fullname}\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n\t<a href=\"https://technitium.com/dns/\">\n\t\t<img src=\"https://technitium.com/img/logo.png\" alt=\"Technitium DNS Server\" /><br />\n\t\t<b>Technitium DNS Server</b>\n\t</a><br />\n\t<br />\n\t<b>Self host a DNS server for privacy & security</b><br />\n\t<b>Block ads & malware at DNS level for your entire network!</b>\n</p>\n<p align=\"center\">\n<img src=\"https://technitium.com/dns/ScreenShot1.png\" alt=\"Technitium DNS Server\" />\n</p>\n\nTechnitium 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.\n\nNobody 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.\n\nBe 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.\n\n# Sponsored By\n<p align=\"center\">\n\t<a href=\"https://althatech.com/\" target=\"_blank\"><img src=\"https://technitium.com/img/logo-althatech.png\" width=\"250\" alt=\"Altha Technology - Censorship Resistant Data Services\" title=\"Altha Technology - Censorship Resistant Data Services\" /></a>\n</p>\n<p align=\"center\">\n\t<a href=\"https://www.bartellhotels.com/\" target=\"_blank\"><img src=\"https://technitium.com/img/logo-bartell-hotels.png\" width=\"300\" alt=\"Bartell Hotels - San Diego's Unforgettable Locations\" title=\"Bartell Hotels - San Diego's Unforgettable Locations\" /></a>\n\t<a href=\"https://www.wavspeed.com/\" target=\"_blank\"><img src=\"https://technitium.com/img/logo-wavspeed.png\" width=\"350\" alt=\"Technology Investors and Integrators | WavSpeed Inc | Texas\" title=\"Technology Investors and Integrators | WavSpeed Inc | Texas\" /></a>\n</p>\n\n# Features\n- Works on Windows, Linux, macOS and Raspberry Pi.\n- Docker image available on [Docker Hub](https://hub.docker.com/r/technitium/dns-server).\n- Installs in just a minute and works out-of-the-box with zero configuration.\n- Block ads & malware using one or more block list URLs.\n- Supports working as an authoritative as well as a recursive DNS server.\n- Includes built-in Clustering feature to allow managing two or more DNS server instances from a single admin web console.\n- 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).\n- 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.\n- DNS-over-HTTPS implementation supports HTTP/1.1, HTTP/2, and HTTP/3 transport protocols.\n- 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.\n- 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.\n- Support for latency based name server selection algorithm that works with concurrency feature for both recursive resolution and forwarders.\n- Advanced caching with features like serve stale, prefetching and auto prefetching.\n- Persistent caching feature that saves cache to disk when DNS server restarts.\n- DNS rebinding attack protection feature available with DNS Rebinding Protection App.\n- DNSSEC validation support with RSA, ECDSA & EdDSA algorithms for recursive resolver, forwarders, and conditional forwarders with NSEC and NSEC3 support.\n- DNSSEC support for all supported DNS transport protocols including encrypted DNS protocols.\n- 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.\n- SVCB & HTTPS [draft-ietf-dnsop-svcb-https](https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-12.html) record type support.\n- URI [RFC 7553](https://www.rfc-editor.org/rfc/rfc7553.html) record type support.\n- SSHFP [RFC 4255](https://www.rfc-editor.org/rfc/rfc4255.html) record type support.\n- CNAME cloaking feature to block domain names that resolve to CNAME which are blocked.\n- QNAME minimization support in recursive resolver [RFC 9156](https://www.rfc-editor.org/rfc/rfc9156.html).\n- QNAME case randomization support for UDP transport protocol [draft-vixie-dnsext-dns0x20-00](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00).\n- DNAME record [RFC 6672](https://datatracker.ietf.org/doc/html/rfc6672) support.\n- 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.\n- 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.\n- Support for features like Split Horizon and Geolocation based responses using DNS Apps feature.\n- Support for REGEX based block lists with different block lists for different client IP addresses or subnet using Advanced Blocking DNS App.\n- Primary, Secondary, Stub, and Conditional Forwarder zone support.\n- Static stub zone support implemented in Conditional Forwarder zone to force a domain name to resolve via given name servers using NS records.\n- Supports Catalog Zones [RFC 9432](https://datatracker.ietf.org/doc/rfc9432/).\n- Supports record aging where the records with expiry set are automatically removed from the zone.\n- Bulk conditional forwarding support using Advanced Forwarding DNS App.\n- DNSSEC signed zones support with RSA, ECDSA & EdDSA algorithms.\n- DNSSEC support for both NSEC and NSEC3.\n- 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.\n- Zone transfer over TLS (XFR-over-TLS) [RFC 9103](https://www.rfc-editor.org/rfc/rfc9103.html) support.\n- Zone transfer over QUIC (XFR-over-QUIC) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html) support.\n- Support for zone validation using ZONEMD records [RFC 8976](https://datatracker.ietf.org/doc/rfc8976/) for Secondary zones.\n- Dynamic DNS Updates [RFC 2136](https://www.rfc-editor.org/rfc/rfc2136) support with security policy.\n- Secret key transaction authentication (TSIG) [RFC 8945](https://datatracker.ietf.org/doc/html/rfc8945) support for zone transfers.\n- EDNS(0) [RFC6891](https://datatracker.ietf.org/doc/html/rfc6891) support.\n- EDNS Client Subnet (ECS) [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871) support for recursive resolution and forwarding.\n- Extended DNS Errors [RFC 8914](https://datatracker.ietf.org/doc/html/rfc8914) support.\n- DNS64 function [RFC 6147](https://www.rfc-editor.org/rfc/rfc6147) support for use by IPv6 only clients using the DNS64 App.\n- Support to host DNSBL / RBL block lists [RFC 5782](https://www.rfc-editor.org/rfc/rfc5782).\n- Multi-user role based access with non-expiring API token support.\n- Self host your domain names on your own DNS server.\n- Wildcard sub domain support.\n- Enable/disable zones and records to allow testing with ease.\n- Built-in DNS Client with option to import responses to local zone.\n- 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).\n- Built-in DHCP Server that can work for multiple networks.\n- IPv6 support in DNS server core.\n- 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/).\n- Admin web console for easy configuration using any web browser with support for Dark Mode.\n- Built in HTTP API to allow 3rd party apps to control and configure the DNS server.\n- Supports TOTP based Two-factor authentication (2FA).\n- Built-in system logging and query logging.\n- Open source cross-platform .NET 9 implementation hosted on [GitHub](https://github.com/TechnitiumSoftware/DnsServer).\n\n# Installation\n- **Windows**: [Download setup installer](https://download.technitium.com/dns/DnsServerSetup.zip) for easy installation.\n- **Linux & Raspberry Pi**: Follow install instructions from [this blog post](https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html).\n- **Cross-Platform**: [Download portable app](https://download.technitium.com/dns/DnsServerPortable.tar.gz) to run on any platform that has .NET 9 installed.\n- **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).\n\n# Build Instructions\nYou 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).\n\n# Docker Environment Variables\nTechnitium 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.\n\n# API Documentation\nThe 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.\n\n# Help Topics\nRead the latest [online help topics](https://go.technitium.com/?id=25) which contains the DNS Server user manual and covers frequently asked questions.\n\n# Support\nFor 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).\n\nJoin [/r/technitium](https://www.reddit.com/r/technitium/) on Reddit.\n\n# Donate\nMake contribution to Technitium and help making new software, updates, and features possible.\n\n[Donate Now!](https://www.patreon.com/technitium)\n\n# Blog Posts\n- [Technitium Blog: Understanding Clustering And How To Configure It](https://blog.technitium.com/2025/11/understanding-clustering-and-how-to.html) (Nov 2025)\n- [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)\n- [Technitium Blog: Technitium DNS Server v14 Released!](https://blog.technitium.com/2025/11/technitium-dns-server-v14-released.html) (Nov 2025)\n- [XDA: Technitium is the best local DNS tool you can deploy](https://www.xda-developers.com/technitium-best-local-dns-tool/) (Aug 2025)\n- [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)\n- [Technitium Blog: Technitium DNS Server v13 Released!](https://blog.technitium.com/2024/09/technitium-dns-server-v13-released.html) (Sept 2024)\n- [Technitium Blog: Technitium DNS Server v12 Released!](https://blog.technitium.com/2024/02/technitium-dns-server-v12-released.html) (Feb 2024)\n- [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)\n- [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)\n- [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)\n- [Technitium Blog: Technitium DNS Server v11 Released!](https://blog.technitium.com/2023/02/technitium-dns-server-v11-released.html) (Feb 2023)\n- [Technitium Blog: Technitium DNS Server v10 Released!](https://blog.technitium.com/2022/11/technitium-dns-server-v10-released.html) (Nov 2022)\n- [Technitium Blog: Technitium DNS Server v9 Released!](https://blog.technitium.com/2022/09/technitium-dns-server-v9-released.html) (Sept 2022)\n- [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)\n- [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)\n- [Technitium Blog: Technitium DNS Server v8 Released!](https://blog.technitium.com/2022/03/technitium-dns-server-v8-released.html) (Mar 2022)\n- [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)\n- [Yolan Romailler: Being ad-free on Android without rooting](https://romailler.ch/2021/04/15/misc-pihole_over_dot/) (Apr 2021)\n- [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)\n- [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)\n- [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)\n- [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)\n- [Technitium Blog: Technitium DNS Server v5 Released!](https://blog.technitium.com/2020/07/technitium-dns-server-v5-released.html) (Jul 2020)\n- [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)\n- [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)\n- [Scott Hanselman: Exploring DNS with the .NET Core based Technitium DNS Server](https://www.hanselman.com/blog/ExploringDNSWithTheNETCoreBasedTechnitiumDNSServer.aspx) (Apr 2019)\n- [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)\n- [Technitium Blog: Blocking Internet Ads Using DNS Sinkhole](https://blog.technitium.com/2018/10/blocking-internet-ads-using-dns-sinkhole.html) (Oct 2018)\n- [Technitium Blog: Configuring DNS Server For Privacy & Security](https://blog.technitium.com/2018/06/configuring-dns-server-for-privacy.html) (Jun 2018)\n- [Technitium Blog: Technitium DNS Server v1.3 Released!](https://blog.technitium.com/2018/06/technitium-dns-server-v13-released.html) (Jun 2018)\n- [Technitium Blog: Running Technitium DNS Server on Ubuntu Linux](https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html) (Nov 2017)\n- [Technitium Blog: Technitium DNS Server Released!](https://blog.technitium.com/2017/11/technitium-dns-server-released.html) (Nov 2017)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nOnly the latest available version of Technitium DNS Server is supported for security updates.\n\n## Reporting a Vulnerability\n\nTo report a vulnerability send an email to security@technitium.com\n"
  },
  {
    "path": "build.md",
    "content": "# Build Instructions\n\n## For Windows\n\nTo 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:\n\n1. 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.\n\n2. 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.\n\n3. 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.\n\n## For Linux\n\nFollow 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.\n\n1. Install prerequisites like curl and git.\n```\nsudo apt update\nsudo apt install curl git -y\n```\n\n2. 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:\n\n- Ubuntu 24.04\n```\nsudo add-apt-repository ppa:dotnet/backports\nsudo apt update\n```\n\n- Raspberry Pi OS\n```\ncurl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -\nsudo apt-add-repository https://packages.microsoft.com/debian/11/prod\nsudo apt update\n```\n\n3. Install ASP.NET Core 9 SDK and `libmsquic` for DNS-over-QUIC support.\n```\nsudo apt install dotnet-sdk-9.0 libmsquic -y\n```\n\nNote! 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`.\n\n4. Clone the source code for both [TechnitiumLibrary](https://github.com/TechnitiumSoftware/TechnitiumLibrary) and [DnsServer](https://github.com/TechnitiumSoftware/DnsServer) into the current folder.\n```\ngit clone --depth 1 https://github.com/TechnitiumSoftware/TechnitiumLibrary.git TechnitiumLibrary\ngit clone --depth 1 https://github.com/TechnitiumSoftware/DnsServer.git DnsServer\n```\n\n5. Build the TechnitiumLibrary source.\n```\ndotnet build TechnitiumLibrary/TechnitiumLibrary.ByteTree/TechnitiumLibrary.ByteTree.csproj -c Release\ndotnet build TechnitiumLibrary/TechnitiumLibrary.Net/TechnitiumLibrary.Net.csproj -c Release\ndotnet build TechnitiumLibrary/TechnitiumLibrary.Security.OTP/TechnitiumLibrary.Security.OTP.csproj -c Release\n```\n\n6. Build the DnsServer source.\n```\ndotnet publish DnsServer/DnsServerApp/DnsServerApp.csproj -c Release\n```\n\n7. Install the DNS server as a systemd service.\n\nNote! Skip this step if you wish to build and use docker image.\n\n```\nsudo mkdir -p /opt/technitium/dns\nsudo cp -r DnsServer/DnsServerApp/bin/Release/publish/* /opt/technitium/dns\nsudo cp /opt/technitium/dns/systemd.service /etc/systemd/system/dns.service\nsudo systemctl stop systemd-resolved\nsudo systemctl disable systemd-resolved\nsudo systemctl enable dns.service\nsudo systemctl start dns.service\nsudo rm /etc/resolv.conf\necho \"nameserver 127.0.0.1\" | sudo tee /etc/resolv.conf\n```\n\n8. Build and run docker image.\n\nNote! Skip this step if you have already installed the DNS server as a systemd service in previous step.\n\nNote! Before proceeding to build a Docker image, it is required that you have installed `docker` on your computer.\n\nFollow the commands given below to build a docker image for the DNS server.\n\n```\ncd DnsServer\nsudo docker build -t technitium/dns-server:latest .\n```\n\nYou 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.\n\n```\nsudo systemctl stop systemd-resolved\nsudo systemctl disable systemd-resolved\nsudo docker compose up -d\n```\n\n9. Open the DNS server web console in a web browser using `http://<server-ip-address>:5380/` URL and set a login password to complete the installation.\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  dns-server:\n    container_name: dns-server\n    hostname: dns-server\n    image: technitium/dns-server:latest\n    # For DHCP deployments, use \"host\" network mode and remove all the port mappings, including the ports array by commenting them\n    # network_mode: \"host\"\n    ports:\n      - \"5380:5380/tcp\" #DNS web console (HTTP)\n      # - \"53443:53443/tcp\" #DNS web console (HTTPS)\n      - \"53:53/udp\" #DNS service\n      - \"53:53/tcp\" #DNS service\n      # - \"853:853/udp\" #DNS-over-QUIC service\n      # - \"853:853/tcp\" #DNS-over-TLS service\n      # - \"443:443/udp\" #DNS-over-HTTPS service (HTTP/3)\n      # - \"443:443/tcp\" #DNS-over-HTTPS service (HTTP/1.1, HTTP/2)\n      # - \"80:80/tcp\" #DNS-over-HTTP service (use with reverse proxy or certbot certificate renewal)\n      # - \"8053:8053/tcp\" #DNS-over-HTTP service (use with reverse proxy)\n      # - \"67:67/udp\" #DHCP service\n    environment:\n      - DNS_SERVER_DOMAIN=dns-server #The primary domain name used by this DNS Server to identify itself.\n      # - DNS_SERVER_ADMIN_PASSWORD=password #DNS web console admin user password.\n      # - 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.\n      # - DNS_SERVER_PREFER_IPV6=false #DNS Server will use IPv6 for querying whenever possible with this option enabled.\n      # - 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.\n      # - DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380 #The TCP port number for the DNS web console over HTTP protocol.\n      # - DNS_SERVER_WEB_SERVICE_HTTPS_PORT=53443 #The TCP port number for the DNS web console over HTTPS protocol.\n      # - DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS=false #Enables HTTPS for the DNS web console.\n      # - DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT=false #Enables self signed TLS certificate for the DNS web console.\n      # - DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PATH=/etc/dns/tls/cert.pfx #The file path to the TLS certificate for the DNS web console.\n      # - DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PASSWORD=password #The password for the TLS certificate for the DNS web console.\n      # - DNS_SERVER_WEB_SERVICE_HTTP_TO_TLS_REDIRECT=false #Enables HTTP to HTTPS redirection for the DNS web console.\n      # - 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.\n      # - DNS_SERVER_RECURSION=AllowOnlyForPrivateNetworks #Recursion options: Allow, Deny, AllowOnlyForPrivateNetworks, UseSpecifiedNetworkACL.\n      # - 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.\n      # - 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.\n      # - 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.\n      # - DNS_SERVER_ENABLE_BLOCKING=false #Sets the DNS server to block domain names using Blocked Zone and Block List Zone.\n      # - 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.\n      # - DNS_SERVER_BLOCK_LIST_URLS= #A comma separated list of block list URLs.\n      # - DNS_SERVER_FORWARDERS=1.1.1.1, 8.8.8.8 #Comma separated list of forwarder addresses.\n      # - DNS_SERVER_FORWARDER_PROTOCOL=Tcp #Forwarder protocol options: Udp, Tcp, Tls, Https, HttpsJson.\n      # - DNS_SERVER_LOG_USING_LOCAL_TIME=true #Enable this option to use local time instead of UTC for logging.\n    volumes:\n      - config:/etc/dns\n    restart: unless-stopped\n    sysctls:\n      - net.ipv4.ip_local_port_range=1024 65535\n\nvolumes:\n    config:\n"
  }
]